Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
173 KB
Referenced Files
None
Subscribers
None
diff --git a/src/Game.cpp b/src/Game.cpp
index 0acfab4..01bde5e 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -1,2075 +1,2131 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Block.h"
#include "GameState.h"
#include "Functions.h"
#include "Settings.h"
#include "GameObjects.h"
#include "ThemeManager.h"
#include "Game.h"
#include "WordWrapper.h"
#include "LevelEditor.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "InputManager.h"
#include "MusicManager.h"
#include "Render.h"
#include "StatisticsManager.h"
#include "ScriptExecutor.h"
#include "MD5.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include <map>
#include <algorithm>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <SDL_ttf_fontfallback.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
const char* Game::blockName[TYPE_MAX]={"Block","PlayerStart","ShadowStart",
"Exit","ShadowBlock","Spikes",
"Checkpoint","Swap","Fragile",
"MovingBlock","MovingShadowBlock","MovingSpikes",
"Teleporter","Button","Switch",
"ConveyorBelt","ShadowConveyorBelt","NotificationBlock", "Collectable", "Pushable"
};
map<string,int> Game::blockNameMap;
map<int,string> Game::gameObjectEventTypeMap;
map<string,int> Game::gameObjectEventNameMap;
map<int,string> Game::levelEventTypeMap;
map<string,int> Game::levelEventNameMap;
string Game::recordFile;
//An internal function.
static void copyCompiledScripts(lua_State *state, const std::map<int, int>& src, std::map<int, int>& dest) {
//Clear the existing scripts.
for (auto it = dest.begin(); it != dest.end(); ++it) {
luaL_unref(state, LUA_REGISTRYINDEX, it->second);
}
dest.clear();
//Copy the source to the destination.
for (auto it = src.begin(); it != src.end(); ++it) {
lua_rawgeti(state, LUA_REGISTRYINDEX, it->second);
dest[it->first] = luaL_ref(state, LUA_REGISTRYINDEX);
}
}
//An internal function.
static void copyLevelObjects(const std::vector<Block*>& src, std::vector<Block*>& dest, bool setActive) {
//Clear the existing objects.
for (auto o : dest) delete o;
dest.clear();
//Copy the source to the destination.
for (auto o : src) {
if (o == NULL || o->isDelete) continue;
Block* o2 = new Block(*o);
dest.push_back(o2);
if (setActive) o2->setActive();
}
}
Game::Game(SDL_Renderer &renderer, ImageManager &imageManager):isReset(false)
, scriptExecutor(new ScriptExecutor())
,currentLevelNode(NULL)
,customTheme(NULL)
,background(NULL)
- , levelRect(SDL_Rect{ 0, 0, 0, 0 }), levelRectSaved(SDL_Rect{ 0, 0, 0, 0 }), levelRectInitial(SDL_Rect{ 0, 0, 0, 0 })
+ , levelRect(SDL_Rect{ 0, 0, 0, 0 }), levelRectInitial(SDL_Rect{ 0, 0, 0, 0 })
, arcade(false)
,won(false)
,interlevel(false)
- ,time(0),timeSaved(0)
- ,recordings(0),recordingsSaved(0)
- ,cameraMode(CAMERA_PLAYER),cameraModeSaved(CAMERA_PLAYER)
+ ,time(0)
+ ,recordings(0)
+ ,cameraMode(CAMERA_PLAYER)
,player(this),shadow(this),objLastCheckPoint(NULL)
- , currentCollectables(0), currentCollectablesSaved(0), currentCollectablesInitial(0)
- , totalCollectables(0), totalCollectablesSaved(0), totalCollectablesInitial(0)
+ , currentCollectables(0), currentCollectablesInitial(0)
+ , totalCollectables(0), totalCollectablesInitial(0)
{
+ levelRectSaved = SDL_Rect{ 0, 0, 0, 0 };
+ timeSaved = 0;
+ recordingsSaved = 0;
+ cameraModeSaved = (int)CAMERA_PLAYER;
+ currentCollectablesSaved = 0;
+ totalCollectablesSaved = 0;
saveStateNextTime=false;
loadStateNextTime=false;
recentSwap=recentSwapSaved=-10000;
recentLoad=recentSave=0;
action=imageManager.loadTexture(getDataPath()+"gfx/actions.png", renderer);
medals=imageManager.loadTexture(getDataPath()+"gfx/medals.png", renderer);
//Get the collectable image from the theme.
//NOTE: Isn't there a better way to retrieve the image?
objThemes.getBlock(TYPE_COLLECTABLE)->createInstance(&collectable);
//Hide the cursor if not in the leveleditor.
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
Game::~Game(){
//Simply call our destroy method.
destroy();
//Before we leave make sure the cursor is visible.
SDL_ShowCursor(SDL_ENABLE);
}
void Game::destroy(){
delete scriptExecutor;
scriptExecutor = NULL;
//Loop through the levelObjects, etc. and delete them.
for (auto o : levelObjects) delete o;
levelObjects.clear();
for (auto o : levelObjectsSave) delete o;
levelObjectsSave.clear();
for (auto o : levelObjectsInitial) delete o;
levelObjectsInitial.clear();
//Loop through the sceneryLayers and delete them.
for(auto it=sceneryLayers.begin();it!=sceneryLayers.end();++it){
delete it->second;
}
sceneryLayers.clear();
//Clear the name and the editor data.
levelName.clear();
levelFile.clear();
scriptFile.clear();
editorData.clear();
//Remove everything from the themeManager.
background=NULL;
if(customTheme)
objThemes.removeTheme();
customTheme=NULL;
//If there's a (partial) theme bundled with the levelpack remove that as well.
if(levels->customTheme)
objThemes.removeTheme();
//delete current level (if any)
if(currentLevelNode){
delete currentLevelNode;
currentLevelNode=NULL;
}
//Reset the time.
time=timeSaved=0;
recordings=recordingsSaved=0;
recentSwap=recentSwapSaved=-10000;
//Set the music list back to the configured list.
getMusicManager()->setMusicList(getSettings()->getValue("musiclist"));
}
void Game::reloadMusic() {
//NOTE: level music is always enabled.
//Check if the levelpack has a prefered music list.
if (levels && !levels->levelpackMusicList.empty())
getMusicManager()->setMusicList(levels->levelpackMusicList);
//Check for the music to use.
string &s = editorData["music"];
if (!s.empty()) {
getMusicManager()->playMusic(s);
} else {
getMusicManager()->pickMusic();
}
}
void Game::loadLevelFromNode(ImageManager& imageManager, SDL_Renderer& renderer, TreeStorageNode* obj, const string& fileName, const std::string& scriptFileName){
//Make sure there's nothing left from any previous levels.
assert(levelObjects.empty() && levelObjectsSave.empty() && levelObjectsInitial.empty());
//set current level to loaded one.
currentLevelNode=obj;
//Set the level dimensions to the default, it will probably be changed by the editorData,
//but 800x600 is a fallback.
levelRect = levelRectSaved = levelRectInitial = SDL_Rect{ 0, 0, 800, 600 };
currentCollectables = currentCollectablesSaved = currentCollectablesInitial = 0;
totalCollectables = totalCollectablesSaved = totalCollectablesInitial = 0;
//Load the additional data.
for(map<string,vector<string> >::iterator i=obj->attributes.begin();i!=obj->attributes.end();++i){
if(i->first=="size"){
//We found the size attribute.
if(i->second.size()>=2){
//Set the dimensions of the level.
int w = atoi(i->second[0].c_str()), h = atoi(i->second[1].c_str());
levelRect = levelRectSaved = levelRectInitial = SDL_Rect{ 0, 0, w, h };
}
}else if(i->second.size()>0){
//Any other data will be put into the editorData.
editorData[i->first]=i->second[0];
}
}
//Get the arcade property.
{
string &s = editorData["arcade"];
arcade = atoi(s.c_str()) != 0;
}
//Get the theme.
{
//NOTE: level themes are always enabled.
//Check for bundled (partial) themes for level pack.
if (levels->customTheme){
if (objThemes.appendThemeFromFile(levels->levelpackPath + "/theme/theme.mnmstheme", imageManager, renderer) == NULL){
//The theme failed to load so set the customTheme boolean to false.
levels->customTheme = false;
}
}
//Check for the theme to use for this level. This has higher priority.
//Examples: %DATA%/themes/classic or %USER%/themes/Orange
string &s = editorData["theme"];
if (!s.empty()){
customTheme = objThemes.appendThemeFromFile(processFileName(s) + "/theme.mnmstheme", imageManager, renderer);
}
//Set the Appearance of the player and the shadow.
objThemes.getCharacter(false)->createInstance(&player.appearance, "standright");
player.appearanceInitial = player.appearanceSave = player.appearance;
objThemes.getCharacter(true)->createInstance(&shadow.appearance, "standright");
shadow.appearanceInitial = shadow.appearanceSave = shadow.appearance;
}
//Get the music.
reloadMusic();
//Load the data from the level node.
for(unsigned int i=0;i<obj->subNodes.size();i++){
TreeStorageNode* obj1=obj->subNodes[i];
if(obj1==NULL) continue;
if(obj1->name=="tile"){
Block* block=new Block(this);
if(!block->loadFromNode(imageManager,renderer,obj1)){
delete block;
continue;
}
//If the type is collectable, increase the number of totalCollectables
if (block->type == TYPE_COLLECTABLE) {
totalCollectablesSaved = totalCollectablesInitial = ++totalCollectables;
}
//Add the block to the levelObjects vector.
levelObjects.push_back(block);
}else if(obj1->name=="scenerylayer" && obj1->value.size()==1){
std::string layerName = obj1->value[0];
//Upgrade the layer naming convention.
if (layerName >= "f") {
//Foreground layer.
if (layerName.size() < 3 || layerName[0] != 'f' || layerName[1] != 'g' || layerName[2] != '_') {
layerName = "fg_" + layerName;
}
} else {
//Background layer.
if (layerName.size() < 3 || layerName[0] != 'b' || layerName[1] != 'g' || layerName[2] != '_') {
layerName = "bg_" + layerName;
}
}
//Check if the layer exists.
if (sceneryLayers[layerName] == NULL) {
sceneryLayers[layerName] = new SceneryLayer();
}
//Load contents from node.
sceneryLayers[layerName]->loadFromNode(this, imageManager, renderer, obj1);
}else if(obj1->name=="script" && !obj1->value.empty()){
map<string,int>::iterator it=Game::levelEventNameMap.find(obj1->value[0]);
if(it!=Game::levelEventNameMap.end()){
int eventType=it->second;
const std::string& script=obj1->attributes["script"][0];
if(!script.empty()) scripts[eventType]=script;
}
}
}
//Set the levelName to the name of the current level.
levelName=editorData["name"];
levelFile=fileName;
scriptFile = scriptFileName;
//Some extra stuff only needed when not in the levelEditor.
if(stateID!=STATE_LEVEL_EDITOR){
//We create a text with the text "Level <levelno> <levelName>".
//It will be shown in the left bottom corner of the screen.
string s;
if(levels->getLevelCount()>1 && levels->type!=COLLECTION){
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),editorData["name"]));
} else {
s = _CC(levels->getDictionaryManager(), editorData["name"]);
}
bmTipsLevelName=textureFromText(renderer, *fontText,s.c_str(),objThemes.getTextColor(true));
}
//Get the background
background=objThemes.getBackground(false);
//Now the loading is finished, we reset all objects to their initial states.
//Before doing that we swap the levelObjects to levelObjectsInitial.
std::swap(levelObjects, levelObjectsInitial);
reset(true, stateID == STATE_LEVEL_EDITOR);
}
void Game::loadLevel(ImageManager& imageManager,SDL_Renderer& renderer,std::string fileName){
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode *obj=new TreeStorageNode();
{
POASerializer objSerializer;
string s=fileName;
//Parse the file.
if(!objSerializer.loadNodeFromFile(s.c_str(),obj,true)){
cerr<<"ERROR: Can't load level file "<<s<<endl;
delete obj;
return;
}
}
//Now call another function.
loadLevelFromNode(imageManager, renderer, obj, fileName, std::string());
}
void Game::saveRecord(const char* fileName){
//check if current level is NULL (which should be impossible)
if(currentLevelNode==NULL) return;
TreeStorageNode obj;
POASerializer objSerializer;
//put current level to the node.
currentLevelNode->name="map";
obj.subNodes.push_back(currentLevelNode);
//put the random seed into the attributes.
obj.attributes["seed"].push_back(prngSeed);
//put the external script file into the attributes.
{
std::string s = scriptFile;
if (s.empty() && !levelFile.empty() && levelFile[0] != '?') {
s = levelFile;
size_t lp = s.find_last_of('.');
if (lp != std::string::npos) {
s = s.substr(0, lp);
}
s += ".lua";
}
//Now load the external script file.
if (!s.empty()) {
if (s[0] == '?') {
s = s.substr(1);
} else {
FILE *f = fopen(s.c_str(), "r");
if (f) {
fseek(f, 0, SEEK_END);
size_t m = ftell(f);
fseek(f, 0, SEEK_SET);
std::vector<char> tmp(m);
fread(tmp.data(), 1, m, f);
fclose(f);
tmp.push_back(0);
s = tmp.data();
} else {
s.clear();
}
}
}
if (!s.empty()) {
obj.attributes["script"].push_back(s);
}
}
//serialize the game record using RLE compression.
#define PUSH_BACK \
if(j>0){ \
if(j>1){ \
sprintf(c,"%d*%d",last,j); \
}else{ \
sprintf(c,"%d",last); \
} \
v.push_back(c); \
}
vector<string> &v=obj.attributes["record"];
vector<int> *record=player.getRecord();
char c[64];
int i,j=0,last;
for(i=0;i<(int)record->size();i++){
int currentKey=(*record)[i];
if(j==0 || currentKey!=last){
PUSH_BACK;
last=currentKey;
j=1;
}else{
j++;
}
}
PUSH_BACK;
#undef PUSH_BACK
#ifdef RECORD_FILE_DEBUG
//add record file debug data.
{
obj.attributes["recordKeyPressLog"].push_back(player.keyPressLog());
vector<SDL_Rect> &playerPosition=player.playerPosition();
string s;
char c[32];
sprintf(c,"%d\n",int(playerPosition.size()));
s=c;
for(unsigned int i=0;i<playerPosition.size();i++){
SDL_Rect& r=playerPosition[i];
sprintf(c,"%d %d\n",r.x,r.y);
s+=c;
}
obj.attributes["recordPlayerPosition"].push_back(s);
}
#endif
//save it
objSerializer.saveNodeToFile(fileName,&obj,true,true);
//remove current level from node to prevent delete it.
obj.subNodes.clear();
}
/////////////EVENT///////////////
void Game::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//First of all let the player handle input.
player.handleInput(&shadow);
//Check for an SDL_QUIT event.
if(event.type==SDL_QUIT){
//We need to quit so enter STATE_EXIT.
setNextState(STATE_EXIT);
}
//Check for the escape key.
if(stateID != STATE_LEVEL_EDITOR && inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
//Escape means we go one level up, to the level select state.
setNextState(STATE_LEVEL_SELECT);
//Save the progress if we are not watching a replay.
if (!player.isPlayFromRecord() || interlevel) {
levels->saveLevelProgress();
}
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}
//Check if 'R' is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_RESTART)){
//Restart game only if we are not watching a replay.
if (!player.isPlayFromRecord() || interlevel) {
//Reset the game at next frame.
isReset = true;
//Also delete any gui (most likely the interlevel gui). Only in game mode.
if (GUIObjectRoot && stateID != STATE_LEVEL_EDITOR){
delete GUIObjectRoot;
GUIObjectRoot = NULL;
}
//And set interlevel to false.
interlevel = false;
}
}
//Check for the next level buttons when in the interlevel popup.
if (inputMgr.isKeyDownEvent(INPUTMGR_SPACE) || inputMgr.isKeyDownEvent(INPUTMGR_SELECT)){
if(interlevel){
//The interlevel popup is shown so we need to delete it.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Now goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
//Check if tab is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_TAB)){
//Switch the camera mode.
switch(cameraMode){
case CAMERA_PLAYER:
cameraMode=CAMERA_SHADOW;
break;
case CAMERA_SHADOW:
case CAMERA_CUSTOM:
cameraMode=CAMERA_PLAYER;
break;
}
}
}
/////////////////LOGIC///////////////////
void Game::logic(ImageManager& imageManager, SDL_Renderer& renderer){
//Reset the gameTip.
gameTipText.clear();
//Add one tick to the time.
time++;
//NOTE: This code reverts some changes in commit 5f03ae5.
//This is part of old prepareFrame() code.
//This is needed since otherwise the script function block:setLocation() and block:moveTo() are completely broken.
//Later we should rewrite collision system completely which will remove this piece of code.
//NOTE: In new collision system the effect of dx/dy/xVel/yVel should only be restricted in one frame.
for (auto obj : levelObjects) {
switch (obj->type) {
default:
obj->dx = obj->dy = obj->xVel = obj->yVel = 0;
break;
case TYPE_PUSHABLE:
//NOTE: Currently the dx/dy/etc. of pushable blocks are still carry across frames, in order to make the collision system work correct.
break;
case TYPE_CONVEYOR_BELT: case TYPE_SHADOW_CONVEYOR_BELT:
//NOTE: We let the conveyor belt itself to reset its xVel/yVel.
obj->dx = obj->dy = 0;
break;
}
}
//NOTE2: The above code breaks pushable block with moving block in most cases,
//more precisely, if the pushable block is processed before the moving block then things may be broken.
//Therefore later we must process other blocks before moving pushable block.
//Process delay execution scripts.
getScriptExecutor()->processDelayExecution();
//Process any event in the queue.
for(unsigned int idx=0;idx<eventQueue.size();idx++){
//Get the event from the queue.
typeGameObjectEvent &e=eventQueue[idx];
//Check if the it has an id attached to it.
if(e.target){
//NOTE: Should we check if the target still exists???
e.target->onEvent(e.eventType);
}else if(e.flags&1){
//Loop through the levelObjects and give them the event if they have the right id.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
if(levelObjects[i]->id==e.id){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}else{
//Loop through the levelObjects and give them the event.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}
//Done processing the events so clear the queue.
eventQueue.clear();
//Remove levelObjects whose isDelete is true.
{
int j = 0;
for (int i = 0; i < (int)levelObjects.size(); i++) {
if (levelObjects[i] == NULL) {
j++;
} else if (levelObjects[i]->isDelete) {
delete levelObjects[i];
levelObjects[i] = NULL;
j++;
} else if (j > 0) {
levelObjects[i - j] = levelObjects[i];
}
}
if (j > 0) levelObjects.resize(levelObjects.size() - j);
}
//Check if we should save/load state.
//NOTE: This happens after event handling so no eventQueue has to be saved/restored.
- if(saveStateNextTime){
- saveState();
- }else if(loadStateNextTime){
- loadState();
- }
- saveStateNextTime=false;
- loadStateNextTime=false;
-
+ checkSaveLoadState();
+
//Loop through the gameobjects to update them.
for(unsigned int i=0;i<levelObjects.size();i++){
//Send GameObjectEvent_OnEnterFrame event to the script
levelObjects[i]->onEvent(GameObjectEvent_OnEnterFrame);
}
//Let the gameobject handle movement.
{
std::vector<Block*> pushableBlocks;
//First we process blocks which are not pushable blocks.
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->move();
}
}
//Sort pushable blocks by their position, which is an ad-hoc workaround for
//<https://forum.freegamedev.net/viewtopic.php?f=48&t=8047#p77692>.
std::stable_sort(pushableBlocks.begin(), pushableBlocks.end(),
[](const Block* obj1, const Block* obj2)->bool
{
SDL_Rect r1 = const_cast<Block*>(obj1)->getBox(), r2 = const_cast<Block*>(obj2)->getBox();
if (r1.y > r2.y) return true;
else if (r1.y < r2.y) return false;
else return r1.x < r2.x;
});
//Now we process pushable blocks.
for (auto o : pushableBlocks) {
o->move();
}
}
//Also update the scenery.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it){
it->second->updateAnimation();
}
//Also update the animation of the background.
{
//Get a pointer to the background.
ThemeBackground* bg = background;
//Check if the background is null, but there are themes.
if (bg == NULL && objThemes.themeCount() > 0){
//Get the background from the first theme in the stack.
bg = objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
//And if it's the loaded background then also update the animation.
if (bg && bg == background) {
bg->updateAnimation();
}
}
//Let the player store his move, if recording.
player.shadowSetState();
//Let the player give his recording to the shadow, if configured.
player.shadowGiveState(&shadow);
//NOTE: to fix bugs regarding player/shadow swap, we should first process collision of player/shadow then move them
const SDL_Rect playerLastPosition = player.getBox();
const SDL_Rect shadowLastPosition = shadow.getBox();
//NOTE: The following is ad-hoc code to fix shadow on blocked player on conveyor belt bug
if (shadow.holdingOther) {
//We need to process shadow collision first if shadow is holding player.
//Let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow.
shadow.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = shadow.getBox();
//Check collision for player. Transfer the velocity of shadow to it only if the shadow moves its position.
player.collision(levelObjects, (r.x != shadowLastPosition.x || r.y != shadowLastPosition.y) ? &shadow : NULL);
} else {
//Otherwise we process player first.
//Check collision for player.
player.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = player.getBox();
//Now let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow. Transfer the velocity of player to it only if the player moves its position.
shadow.collision(levelObjects, (r.x != playerLastPosition.x || r.y != playerLastPosition.y) ? &player : NULL);
}
//Let the player move.
player.move(levelObjects, playerLastPosition.x, playerLastPosition.y);
//Let the shadow move.
shadow.move(levelObjects, shadowLastPosition.x, shadowLastPosition.y);
//Check collision and stuff for the shadow and player.
player.otherCheck(&shadow);
bool doNotResetWon = false;
//Check if we won.
if(won){
//Check if it's level editor test play
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->updateRecordInPlayMode(imageManager, renderer);
}
}
//Check if it's playing from record
else if (player.isPlayFromRecord() && !interlevel) {
//We don't reset the won to false, and let RecordPlayback class handle it.
doNotResetWon = true;
}else{
//Local copy of interlevel property since the replayPlay() will change it later.
const bool previousInterlevel = interlevel;
//We only update the level statistics when the previous state is not interlevel mode.
if (!previousInterlevel) {
//the string to store auto-save record path.
string bestTimeFilePath, bestRecordingFilePath;
//and if we can't get test path.
bool filePathError = false;
//Get current level
LevelPack::Level *level = levels->getLevel();
//Get previous medal
const int oldMedal = level->getMedal();
//Check if MD5 is changed
bool md5Changed = false;
for (int i = 0; i < 16; i++) {
if (level->md5Digest[i] != level->md5InLevelProgress[i]) {
md5Changed = true;
break;
}
}
//Erase existing record if MD5 is changed
if (md5Changed) {
//print some debug message
#if _DEBUG
cout << "MD5 is changed, old " << Md5::toString(level->md5InLevelProgress);
cout << ", new " << Md5::toString(level->md5Digest) << endl;
#endif
level->time = -1;
level->recordings = -1;
//Update the MD5 in record
memcpy(level->md5InLevelProgress, level->md5Digest, sizeof(level->md5Digest));
}
//Get better time and recordings
const int betterTime = level->getBetterTime(time);
const int betterRecordings = level->getBetterRecordings(level->arcade ? currentCollectables : recordings);
//Get new medal
const int newMedal = level->getMedal(betterTime, betterRecordings);
//Check if we need to update statistics
if (newMedal > oldMedal || md5Changed) {
//Erase statictics for old medal
if (oldMedal > 0) statsMgr.completedLevels--;
if (oldMedal == 2) statsMgr.silverLevels--;
if (oldMedal == 3) statsMgr.goldLevels--;
//Update statistics for new medal
if (newMedal > 0) statsMgr.completedLevels++;
if (newMedal == 2) statsMgr.silverLevels++;
if (newMedal == 3) statsMgr.goldLevels++;
}
//Check the achievement "Complete a level with checkpoint, but without saving"
if (objLastCheckPoint.get() == NULL) {
for (auto obj : levelObjects) {
if (obj->type == TYPE_CHECKPOINT) {
statsMgr.newAchievement("withoutsave");
break;
}
}
}
//Set the current level won.
level->won = true;
if (level->time != betterTime) {
level->time = betterTime;
//save the best-time game record.
if (bestTimeFilePath.empty()){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath, bestRecordingFilePath, true);
}
if (bestTimeFilePath.empty()){
cerr << "ERROR: Couldn't get auto-save record file path" << endl;
filePathError = true;
} else{
saveRecord(bestTimeFilePath.c_str());
}
}
if (level->recordings != betterRecordings) {
level->recordings = betterRecordings;
//save the best-recordings game record.
if (bestRecordingFilePath.empty() && !filePathError){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath, bestRecordingFilePath, true);
}
if (bestRecordingFilePath.empty()){
cerr << "ERROR: Couldn't get auto-save record file path" << endl;
filePathError = true;
} else{
saveRecord(bestRecordingFilePath.c_str());
}
}
//Set the next level unlocked if it exists.
if (levels->getCurrentLevel() + 1 < levels->getLevelCount()){
levels->setLocked(levels->getCurrentLevel() + 1);
}
//And save the progress.
levels->saveLevelProgress();
}
//Now go to the interlevel screen.
replayPlay(imageManager,renderer);
//Update achievements (only when the previous state is not interlevel mode)
if (!previousInterlevel) {
if (levels->levelpackName == "tutorial") statsMgr.updateTutorialAchievements();
statsMgr.updateLevelAchievements();
}
//NOTE: We set isReset false to prevent the user from getting a best time of 0.00s and 0 recordings.
isReset = false;
}
}
if (!doNotResetWon) won = false;
//Check if we should reset.
if (isReset) {
//NOTE: we don't need to reset save ??? it looks like that there are no bugs
reset(false, false);
}
isReset=false;
//Finally we update the animation of player and shadow (was previously in render() function).
shadow.updateAnimation();
player.updateAnimation();
}
+void Game::checkSaveLoadState() {
+ //Check if we should save/load state.
+ //NOTE: This happens after event handling so no eventQueue has to be saved/restored.
+ if (saveStateNextTime){
+ saveState();
+ } else if (loadStateNextTime){
+ loadState();
+ }
+ saveStateNextTime = false;
+ loadStateNextTime = false;
+}
+
/////////////////RENDER//////////////////
void Game::render(ImageManager&,SDL_Renderer &renderer){
//Update the camera (was previously in logic() function).
switch (cameraMode) {
case CAMERA_PLAYER:
player.setMyCamera();
break;
case CAMERA_SHADOW:
shadow.setMyCamera();
break;
case CAMERA_CUSTOM:
//NOTE: The target is (should be) screen size independent so calculate the real target x and y here.
int targetX = cameraTarget.x - (SCREEN_WIDTH / 2);
int targetY = cameraTarget.y - (SCREEN_HEIGHT / 2);
//Move the camera to the cameraTarget.
if (camera.x > targetX) {
camera.x -= (camera.x - targetX) >> 4;
//Make sure we don't go too far.
if (camera.x < targetX)
camera.x = targetX;
} else if (camera.x < targetX) {
camera.x += (targetX - camera.x) >> 4;
//Make sure we don't go too far.
if (camera.x > targetX)
camera.x = targetX;
}
if (camera.y > targetY) {
camera.y -= (camera.y - targetY) >> 4;
//Make sure we don't go too far.
if (camera.y < targetY)
camera.y = targetY;
} else if (camera.y < targetY) {
camera.y += (targetY - camera.y) >> 4;
//Make sure we don't go too far.
if (camera.y > targetY)
camera.y = targetY;
}
break;
}
//First of all render the background.
{
//Get a pointer to the background.
ThemeBackground* bg=background;
//Check if the background is null, but there are themes.
if(bg==NULL && objThemes.themeCount()>0){
//Get the background from the first theme in the stack.
bg=objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
if(bg){
//It isn't so draw it.
bg->draw(renderer);
}else{
//There's no background so fill the screen with white.
SDL_SetRenderDrawColor(&renderer, 255,255,255,255);
SDL_RenderClear(&renderer);
}
}
//Now draw the blackground layers.
auto it = sceneryLayers.begin();
for (; it != sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
it->second->show(renderer);
}
//Now we draw the levelObjects.
{
//NEW: always render the pushable blocks in front of other blocks
std::vector<Block*> pushableBlocks;
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->show(renderer);
}
}
for (auto o : pushableBlocks) {
o->show(renderer);
}
}
//Followed by the player and the shadow.
//NOTE: We draw the shadow first, because he needs to be behind the player.
shadow.show(renderer);
player.show(renderer);
//Now draw the foreground layers.
for (; it != sceneryLayers.end(); ++it){
it->second->show(renderer);
}
//Show the levelName if it isn't the level editor.
if(stateID!=STATE_LEVEL_EDITOR && bmTipsLevelName!=NULL && !interlevel){
SDL_Rect r(rectFromTexture(bmTipsLevelName));
drawGUIBox(-2, SCREEN_HEIGHT - r.h - 4, r.w + 8, r.h + 6, renderer, 0xFFFFFFFF);
applyTexture(2, SCREEN_HEIGHT - r.h, *bmTipsLevelName, renderer, NULL);
}
//Check if there's a tooltip.
if(!gameTipText.empty()){
std::string message = gameTipText;
if (gameTipTexture.needsUpdate(gameTipText)) {
int maxWidth = 0, y = 0;
std::vector<std::string> string_data;
//Trim the message.
{
size_t lps = message.find_first_not_of("\n\r \t");
if (lps == string::npos) {
message.clear(); // it's completely empty
} else {
message = message.substr(lps, message.find_last_not_of("\n\r \t") - lps + 1);
}
}
if (!message.empty()) {
message.push_back('\0');
//Split the message into lines.
for (int lps = 0;;) {
// determine the end of line
int lpe = lps;
for (; message[lpe] != '\n' && message[lpe] != '\r' && message[lpe] != '\0'; lpe++);
string_data.push_back(message.substr(lps, lpe - lps));
// break if the string ends
if (message[lpe] == '\0') break;
// skip "\r\n" for Windows line ending
if (message[lpe] == '\r' && message[lpe + 1] == '\n') lpe++;
// point to the start of next line
lps = lpe + 1;
}
vector<SurfacePtr> lines;
//Create the image for each lines
for (int i = 0; i < (int)string_data.size(); i++) {
//Integer used to center the sentence horizontally.
int w = 0, h = 0;
TTF_SizeUTF8(fontText, string_data[i].c_str(), &w, &h);
//Find out largest width
if (w > maxWidth)
maxWidth = w;
lines.emplace_back(TTF_RenderUTF8_Blended(fontText, string_data[i].c_str(), objThemes.getTextColor(true)));
//Increase y with the height of the text.
y += h;
}
SurfacePtr surf = createSurface(maxWidth, y);
y = 0;
for (auto &s : lines) {
if (s) {
applySurface(0, y, s.get(), surf.get(), NULL);
y += s->h;
}
}
gameTipTexture.update(gameTipText, textureUniqueFromSurface(renderer, std::move(surf)));
}
}
//We already have a gameTip for this type so draw it.
if (!message.empty() && gameTipTexture.get()){
SDL_Rect r(rectFromTexture(*gameTipTexture.get()));
drawGUIBox(-2, -2, r.w + 8, r.h + 6, renderer, 0xFFFFFFFF);
applyTexture(2, 2, *gameTipTexture.get(), renderer);
}
}
// Limit the scope of bm, as it's a borrowed pointer.
{
//Pointer to the sdl texture that will contain a message, if any.
SDL_Texture* bm=NULL;
//Check if the player is dead, meaning we draw a message.
if(player.dead){
//Get user configured restart key
string keyCodeRestart = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_RESTART, false), inputMgr.getKeyCode(INPUTMGR_RESTART, true));
//The player is dead, check if there's a state that can be loaded.
if(player.canLoadState()){
//Now check if the tip is already made, if not make it.
if(bmTipsRestartCheckpoint==NULL){
//Get user defined key for loading checkpoint
string keyCodeLoad = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_LOAD, false), inputMgr.getKeyCode(INPUTMGR_LOAD, true));
//Draw string
bmTipsRestartCheckpoint = textureFromText(renderer, *fontText,//TTF_RenderUTF8_Blended(fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - first %s means currently configured key to restart game
/// - Second %s means configured key to load from last save
tfm::format(_("Press %s to restart current level or press %s to load the game."),
keyCodeRestart,keyCodeLoad).c_str(),
objThemes.getTextColor(true));
}
bm = bmTipsRestartCheckpoint.get();
}else{
//Now check if the tip is already made, if not make it.
if (bmTipsRestart == NULL){
bmTipsRestart = textureFromText(renderer, *fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with currently configured key to restart game
tfm::format(_("Press %s to restart current level."),keyCodeRestart).c_str(),
objThemes.getTextColor(true));
}
bm = bmTipsRestart.get();
}
}
//Check if the shadow has died (and there's no other notification).
//NOTE: We use the shadow's jumptime as countdown, this variable isn't used when the shadow is dead.
if(shadow.dead && bm==NULL && shadow.jumpTime>0){
//Now check if the tip is already made, if not make it.
if(bmTipsShadowDeath==NULL){
bmTipsShadowDeath = textureFromText(renderer, *fontText,
_("Your shadow has died."),
objThemes.getTextColor(true));
}
bm = bmTipsShadowDeath.get();
//NOTE: Logic in the render loop, we substract the shadow's jumptime by one.
shadow.jumpTime--;
//return view to player and keep it there
cameraMode=CAMERA_PLAYER;
}
//Draw the tip.
if(bm!=NULL){
const SDL_Rect textureSize = rectFromTexture(*bm);
int x=(SCREEN_WIDTH-textureSize.w)/2;
int y=32;
drawGUIBox(x-8,y-8,textureSize.w+16,textureSize.h+14,renderer,0xFFFFFFFF);
applyTexture(x,y,*bm,renderer);
}
}
{
int y = SCREEN_HEIGHT;
//Show the number of collectables the user has collected if there are collectables in the level.
//We hide this when interlevel.
if ((currentCollectables || totalCollectables) && !interlevel){
if (arcade) {
//Only show the current collectibles in arcade mode.
if (collectablesTexture.needsUpdate(currentCollectables)) {
collectablesTexture.update(currentCollectables,
textureFromText(renderer,
*fontText,
tfm::format("%d", currentCollectables).c_str(),
objThemes.getTextColor(true)));
}
} else {
if (collectablesTexture.needsUpdate(currentCollectables ^ (totalCollectables << 16))) {
collectablesTexture.update(currentCollectables ^ (totalCollectables << 16),
textureFromText(renderer,
*fontText,
tfm::format("%d/%d", currentCollectables, totalCollectables).c_str(),
objThemes.getTextColor(true)));
}
}
SDL_Rect bmSize = rectFromTexture(*collectablesTexture.get());
//Draw background
drawGUIBox(SCREEN_WIDTH - bmSize.w - 34, SCREEN_HEIGHT - bmSize.h - 4, bmSize.w + 34 + 2, bmSize.h + 4 + 2, renderer, 0xFFFFFFFF);
//Draw the collectable icon
collectable.draw(renderer, SCREEN_WIDTH - 50 + 12, SCREEN_HEIGHT - 50 + 10);
//Draw text
applyTexture(SCREEN_WIDTH - 50 - bmSize.w + 22, SCREEN_HEIGHT - bmSize.h, collectablesTexture.getTexture(), renderer);
y -= bmSize.h + 4;
}
//Show additional message in level editor.
if (stateID == STATE_LEVEL_EDITOR && additionalTexture) {
SDL_Rect r = rectFromTexture(*additionalTexture.get());
r.x = SCREEN_WIDTH - r.w;
r.y = y - r.h;
SDL_SetRenderDrawColor(&renderer, 255, 255, 255, 255);
SDL_RenderFillRect(&renderer, &r);
applyTexture(r.x, r.y, additionalTexture, renderer);
}
}
//show time and records used in level editor or during replay or in arcade mode.
if ((stateID == STATE_LEVEL_EDITOR || (!interlevel && (player.isPlayFromRecord() || arcade)))){
const SDL_Color fg=objThemes.getTextColor(true),bg={255,255,255,255};
const int alpha = 160;
//don't show number of records in arcade mode
if (!arcade && recordingsTexture.needsUpdate(recordings)) {
recordingsTexture.update(recordings,
textureFromTextShaded(
renderer,
*fontMono,
tfm::format(ngettext("%d recording","%d recordings",recordings).c_str(),recordings).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(recordingsTexture.get(),alpha);
}
int y = SCREEN_HEIGHT - (arcade ? 0 : textureHeight(*recordingsTexture.get()));
if (stateID != STATE_LEVEL_EDITOR && bmTipsLevelName != NULL && !interlevel) {
y -= textureHeight(bmTipsLevelName) + 4;
}
if (!arcade) applyTexture(0,y,*recordingsTexture.get(), renderer);
if(timeTexture.needsUpdate(time)) {
timeTexture.update(time,
textureFromTextShaded(
renderer,
*fontMono,
tfm::format("%-.2fs", time / 40.0).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(timeTexture.get(), alpha);
}
y -= textureHeight(*timeTexture.get());
applyTexture(0,y,*timeTexture.get(), renderer);
}
//Draw the current action in the upper right corner.
if(player.record){
const SDL_Rect r = { 0, 0, 50, 50 };
applyTexture(SCREEN_WIDTH - 50, 0, *action, renderer, &r);
} else if (shadow.state != 0){
const SDL_Rect r={50,0,50,50};
applyTexture(SCREEN_WIDTH-50,0,*action,renderer,&r);
}
//if the game is play from record then draw something indicates it
if(player.isPlayFromRecord()){
//Dim the screen if interlevel is true.
if(interlevel){
dimScreen(renderer,191);
}
}else if(auto blockId = player.objNotificationBlock.get()){
//If the player is in front of a notification block and it isn't a replay, show the message.
//Check if we need to update the notification message texture.
//We check against blockId rather than the full message, as blockId is most likely shorter.
if(notificationTexture.needsUpdate(blockId)) {
std::string message = translateAndExpandMessage(blockId->message);
std::vector<std::string> string_data;
//Trim the message.
{
size_t lps = message.find_first_not_of("\n\r \t");
if (lps == string::npos) {
message.clear(); // it's completely empty
} else {
message = message.substr(lps, message.find_last_not_of("\n\r \t") - lps + 1);
}
}
WordWrapper wrapper;
wrapper.font = fontText;
wrapper.maxWidth = SCREEN_WIDTH - 40;
wrapper.wordWrap = true;
wrapper.hyphen = "-";
//Split the message into lines.
wrapper.addString(string_data, message);
//Create the image.
notificationTexture.update(blockId,
textureFromMultilineText(renderer, *fontText, string_data, objThemes.getTextColor(true), GUIGravityCenter));
}
SDL_Rect texSize = rectFromTexture(*notificationTexture.get());
SDL_Rect boxSize = texSize;
const int verticalPadding = 15;
const int verticalPadding2 = 5; // additional padding from the bottom of GUI box to the bottom of screen
boxSize.w += int(SCREEN_WIDTH*0.15);
boxSize.h += verticalPadding * 2;
if (boxSize.w > SCREEN_WIDTH - 20) boxSize.w = SCREEN_WIDTH - 20;
drawGUIBox((SCREEN_WIDTH - boxSize.w) / 2,
SCREEN_HEIGHT - boxSize.h - verticalPadding2 - 3 /* ?? */,
boxSize.w,
boxSize.h + 3 /* ?? */,
renderer, 0xFFFFFFBF);
applyTexture((SCREEN_WIDTH - texSize.w) / 2,
SCREEN_HEIGHT - texSize.h - verticalPadding - verticalPadding2,
notificationTexture.getTexture(), renderer);
}
}
std::string Game::translateAndExpandMessage(const std::string &untranslated_message) {
std::string message = _CC(levels->getDictionaryManager(), untranslated_message);
//Expand the variables.
std::map<std::string, std::string> cachedVariables;
for (;;) {
size_t lps = message.find("{{{");
if (lps == std::string::npos) break;
size_t lpe = message.find("}}}", lps);
if (lpe == std::string::npos) break;
std::string varName = message.substr(lps + 3, lpe - lps - 3), varValue;
auto it = cachedVariables.find(varName);
if (it != cachedVariables.end()) {
varValue = it->second;
} else {
bool isUnknown = true;
if (varName.size() >= 4 && varName.substr(0, 4) == "key_") {
//Probably a key name.
InputManagerKeys key = InputManager::getKeyFromName(varName);
if (key != INPUTMGR_MAX) {
varValue = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(key, false), inputMgr.getKeyCode(key, true));
isUnknown = false;
}
}
if (isUnknown) {
//Unknown variable
cerr << "Warning: Unknown variable '{{{" << varName << "}}}' in notification block message!" << endl;
}
cachedVariables[varName] = varValue;
}
//Substitute.
message.replace(message.begin() + lps, message.begin() + (lpe + 3), varValue);
}
//Over.
return message;
}
void Game::resize(ImageManager&, SDL_Renderer& /*renderer*/){
//Check if the interlevel popup is shown.
if(interlevel && GUIObjectRoot){
GUIObjectRoot->left=(SCREEN_WIDTH-GUIObjectRoot->width)/2;
}
}
void Game::replayPlay(ImageManager& imageManager,SDL_Renderer& renderer){
//Set interlevel true.
interlevel=true;
//Make a copy of the playerButtons.
vector<int> recordCopy=player.recordButton;
//Backup of time and recordings, etc. before we reset the level.
//We choose the same variable names so that we don't need to modify the existing code.
const int time = this->time, recordings = this->recordings, currentCollectables = this->currentCollectables;
//Reset the game.
//NOTE: We don't reset the saves. I'll see that if it will introduce bugs.
reset(false, false);
//Make the cursor visible when the interlevel popup is up.
SDL_ShowCursor(SDL_ENABLE);
//Set the copy of playerButtons back.
player.recordButton=recordCopy;
//Now play the recording.
player.playRecord();
//Create the gui if it isn't already done.
if(!GUIObjectRoot){
//Create a new GUIObjectRoot the size of the screen.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Make child widgets change color properly according to theme.
GUIObjectRoot->inDialog=true;
//Create a GUIFrame for the upper frame.
GUIFrame* upperFrame=new GUIFrame(imageManager,renderer,0,4,0,74);
GUIObjectRoot->addChild(upperFrame);
//Render the You've finished: text and add it to a GUIImage.
//NOTE: The texture is managed by the GUIImage so no need to free it ourselfs.
auto bm = SharedTexture(textureFromText(renderer, *fontGUI,_("You've finished:"),objThemes.getTextColor(true)));
const SDL_Rect textureSize = rectFromTexture(*bm);
GUIImage* title=new GUIImage(imageManager,renderer,0,4-GUI_FONT_RAISE,textureSize.w,textureSize.h,bm);
upperFrame->addChild(title);
//Create the sub title.
string s;
if (levels->getLevelCount()>0){
/// TRANSLATORS: Please do not remove %s or %d from your translation:
/// - %d means the level number in a levelpack
/// - %s means the name of current level
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),levelName));
}
GUIObject* obj=new GUILabel(imageManager,renderer,0,44,0,28,s.c_str(),0,true,true,GUIGravityCenter);
upperFrame->addChild(obj);
obj->render(renderer,0,0,false);
//Determine the width the upper frame should have.
int width;
if(textureSize.w>obj->width)
width=textureSize.w+32;
else
width=obj->width+32;
//Set the left of the title.
title->left=(width-title->width)/2;
//Set the width of the level label to the width of the frame for centering.
obj->width=width;
//Now set the position and width of the frame.
upperFrame->width=width;
upperFrame->left=(SCREEN_WIDTH-width)/2;
//Now create a GUIFrame for the lower frame.
GUIFrame* lowerFrame=new GUIFrame(imageManager,renderer,0,SCREEN_HEIGHT-140,570,135);
GUIObjectRoot->addChild(lowerFrame);
//The different values.
int bestTime=levels->getLevel()->time;
int targetTime=levels->getLevel()->targetTime;
int bestRecordings=levels->getLevel()->recordings;
int targetRecordings=levels->getLevel()->targetRecordings;
int medal = levels->getLevel()->getMedal();
assert(medal > 0);
int maxWidth=0;
int x=20;
//Is there a target time for this level?
int timeY=0;
bool isTargetTime=true;
if(targetTime<0){
isTargetTime=false;
timeY=12;
}
//Create the labels with the time and best time.
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,10+timeY,-1,36,tfm::format(_("Time: %-.2fs"),time/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,34+timeY,-1,36,tfm::format(_("Best time: %-.2fs"),bestTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
if(isTargetTime){
obj=new GUILabel(imageManager,renderer,x,58,-1,36,tfm::format(_("Target time: %-.2fs"),targetTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth+20;
//Is there target recordings for this level?
int recsY=0;
bool isTargetRecs=true;
if(targetRecordings<0){
isTargetRecs=false;
recsY=12;
}
//Now the ones for the recordings.
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj = new GUILabel(imageManager, renderer, x, 10 + recsY, -1, 36,
arcade ? tfm::format(_("Collectibles: %d"), currentCollectables).c_str() : tfm::format(_("Recordings: %d"), recordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj = new GUILabel(imageManager, renderer, x, 34 + recsY, -1, 36,
tfm::format(_(arcade ? "Best collectibles: %d" : "Best recordings: %d"), bestRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
if(isTargetRecs){
obj = new GUILabel(imageManager, renderer, x, 58, -1, 36,
tfm::format(_(arcade ? "Target collectibles: %d" : "Target recordings: %d"), targetRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth;
//The medal that is earned.
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with name of a prize medal (gold, silver or bronze)
string s1=tfm::format(_("You earned the %s medal"),(medal>1)?(medal==3)?_("GOLD"):_("SILVER"):_("BRONZE"));
obj=new GUILabel(imageManager,renderer,50,92,-1,36,s1.c_str(),0,true,true,GUIGravityCenter);
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->left+obj->width>x){
x=obj->left+obj->width+30;
}else{
obj->left=20+(x-20-obj->width)/2;
}
//Create the rectangle for the earned medal.
SDL_Rect r;
r.x=(medal-1)*30;
r.y=0;
r.w=30;
r.h=30;
//Create the medal on the left side.
obj=new GUIImage(imageManager,renderer,16,92,30,30,medals,r);
lowerFrame->addChild(obj);
//And the medal on the right side.
obj=new GUIImage(imageManager,renderer,x-24,92,30,30,medals,r);
lowerFrame->addChild(obj);
//Create the three buttons, Menu, Restart, Next.
/// TRANSLATORS: used as return to the level selector menu
GUIObject* b1=new GUIButton(imageManager,renderer,x,10,-1,36,_("Menu"),0,true,true,GUIGravityCenter);
b1->name="cmdMenu";
b1->eventCallback=this;
lowerFrame->addChild(b1);
b1->render(renderer,0,0,true);
/// TRANSLATORS: used as restart level
GUIObject* b2=new GUIButton(imageManager,renderer,x,50,-1,36,_("Restart"),0,true,true,GUIGravityCenter);
b2->name="cmdRestart";
b2->eventCallback=this;
lowerFrame->addChild(b2);
b2->render(renderer,0,0,true);
/// TRANSLATORS: used as next level
GUIObject* b3=new GUIButton(imageManager,renderer,x,90,-1,36,_("Next"),0,true,true,GUIGravityCenter);
b3->name="cmdNext";
b3->eventCallback=this;
lowerFrame->addChild(b3);
b3->render(renderer,0,0,true);
maxWidth=b1->width;
if(b2->width>maxWidth)
maxWidth=b2->width;
if(b3->width>maxWidth)
maxWidth=b3->width;
b1->left=b2->left=b3->left=x+maxWidth/2;
x+=maxWidth;
lowerFrame->width=x;
lowerFrame->left=(SCREEN_WIDTH-lowerFrame->width)/2;
}
}
bool Game::canSaveState(){
return (player.canSaveState() && shadow.canSaveState());
}
-bool Game::saveState(){
- //Check if the player and shadow can save the current state.
- if(canSaveState()){
- //Let the player and the shadow save their state.
- player.saveState();
- shadow.saveState();
+void Game::saveStateInternal(GameSaveState* o) {
+ //Let the player and the shadow save their state.
+ player.saveStateInternal(&o->playerSaved);
+ shadow.saveStateInternal(&o->shadowSaved);
- //Save the stats.
- timeSaved=time;
- recordingsSaved=recordings;
- recentSwapSaved=recentSwap;
+ //Save the game state.
+ saveGameOnlyStateInternal(&o->gameSaved);
- //Save the PRNG and seed.
- prngSaved = prng;
- prngSeedSaved = prngSeed;
+ //TODO: Save scenery layers, background animation,
+ //state for script executor (mainly delay execution objects),
+ //and results of 'onSave' event
+}
+
+void Game::loadStateInternal(GameSaveState* o) {
+ //Let the player and the shadow load their state.
+ player.loadStateInternal(&o->playerSaved);
+ shadow.loadStateInternal(&o->shadowSaved);
+
+ //Load the game state.
+ loadGameOnlyStateInternal(&o->gameSaved);
+
+ //TODO: load scenery layers, background animation,
+ //state for script executor (mainly delay execution objects),
+ //and results of 'onSave' event
+}
- //Save the level size.
- levelRectSaved = levelRect;
+void Game::saveGameOnlyStateInternal(GameOnlySaveState* o) {
+ //Save the stats.
+ o->timeSaved = time;
+ o->recordingsSaved = recordings;
+ o->recentSwapSaved = recentSwap;
- //Save the camera mode and target.
- cameraModeSaved=cameraMode;
- cameraTargetSaved=cameraTarget;
+ //Save the PRNG and seed.
+ o->prngSaved = prng;
+ o->prngSeedSaved = prngSeed;
- //Save the current collectables
- currentCollectablesSaved = currentCollectables;
- totalCollectablesSaved = totalCollectables;
+ //Save the level size.
+ o->levelRectSaved = levelRect;
- //Save scripts.
- copyCompiledScripts(getScriptExecutor()->getLuaState(), compiledScripts, savedCompiledScripts);
+ //Save the camera mode and target.
+ o->cameraModeSaved = (int)cameraMode;
+ o->cameraTargetSaved = cameraTarget;
- //Save other state, for example moving blocks.
- copyLevelObjects(levelObjects, levelObjectsSave, false);
+ //Save the current collectables
+ o->currentCollectablesSaved = currentCollectables;
+ o->totalCollectablesSaved = totalCollectables;
+
+ //Save scripts.
+ copyCompiledScripts(getScriptExecutor()->getLuaState(), compiledScripts, o->savedCompiledScripts);
+
+ //Save other state, for example moving blocks.
+ copyLevelObjects(levelObjects, o->levelObjectsSave, false);
+
+ //TODO: Save scenery layers, background animation,
+ //state for script executor (mainly delay execution objects),
+ //and results of 'onSave' event
+}
+
+void Game::loadGameOnlyStateInternal(GameOnlySaveState* o) {
+ //Load the stats.
+ time = o->timeSaved;
+ recordings = o->recordingsSaved;
+ recentSwap = o->recentSwapSaved;
+
+ //Load the PRNG and seed.
+ prng = o->prngSaved;
+ prngSeed = o->prngSeedSaved;
+
+ //Load the level size.
+ levelRect = o->levelRectSaved;
+
+ //Load the camera mode and target.
+ cameraMode = (CameraMode)o->cameraModeSaved;
+ cameraTarget = o->cameraTargetSaved;
+
+ //Load the current collactbles
+ currentCollectables = o->currentCollectablesSaved;
+ totalCollectables = o->totalCollectablesSaved;
+
+ //Load scripts.
+ copyCompiledScripts(getScriptExecutor()->getLuaState(), o->savedCompiledScripts, compiledScripts);
+
+ //Load other state, for example moving blocks.
+ copyLevelObjects(o->levelObjectsSave, levelObjects, true);
+
+ //TODO: load scenery layers, background animation,
+ //state for script executor (mainly delay execution objects),
+ //and results of 'onSave' event
+}
+
+bool Game::saveState(){
+ //Check if the player and shadow can save the current state.
+ if(canSaveState()){
+ //Let the player and the shadow save their state.
+ player.saveState();
+ shadow.saveState();
+
+ //Save the game state.
+ saveGameOnlyStateInternal(static_cast<GameOnlySaveState*>(this));
//Also save states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->saveAnimation();
}
//Also save the background animation, if any.
if(background)
background->saveAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentSave+1000>t){
statsMgr.newAchievement("panicSave");
}
recentSave=t;
//Update statistics.
statsMgr.saveTimes++;
//Update achievements
switch(statsMgr.saveTimes){
case 100:
statsMgr.newAchievement("save100");
break;
}
}
//Save the state for script executor.
getScriptExecutor()->saveState();
//Execute the onSave event.
executeScript(LevelEvent_OnSave);
//Return true.
return true;
}
//We can't save the state so return false.
return false;
}
bool Game::loadState(){
//Check if there's a state that can be loaded.
if(player.canLoadState() && shadow.canLoadState()){
//Reset the stats for level editor.
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->currentTime = editor->currentRecordings = -1;
editor->updateAdditionalTexture(getImageManager(), getRenderer());
}
}
//Let the player and the shadow load their state.
player.loadState();
shadow.loadState();
- //Load the stats.
- time=timeSaved;
- recordings=recordingsSaved;
- recentSwap=recentSwapSaved;
-
- //Load the PRNG and seed.
- prng = prngSaved;
- prngSeed = prngSeedSaved;
-
- //Load the level size.
- levelRect = levelRectSaved;
-
- //Load the camera mode and target.
- cameraMode=cameraModeSaved;
- cameraTarget=cameraTargetSaved;
-
- //Load the current collactbles
- currentCollectables = currentCollectablesSaved;
- totalCollectables = totalCollectablesSaved;
-
- //Load scripts.
- copyCompiledScripts(getScriptExecutor()->getLuaState(), savedCompiledScripts, compiledScripts);
-
- //Load other state, for example moving blocks.
- copyLevelObjects(levelObjectsSave, levelObjects, true);
+ //Load the game state.
+ loadGameOnlyStateInternal(static_cast<GameOnlySaveState*>(this));
//Also load states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->loadAnimation();
}
//Also load the background animation, if any.
if(background)
background->loadAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements.
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentLoad+1000>t){
statsMgr.newAchievement("panicLoad");
}
recentLoad=t;
//Update statistics.
statsMgr.loadTimes++;
//Update achievements
switch(statsMgr.loadTimes){
case 100:
statsMgr.newAchievement("load100");
break;
}
}
//Load the state for script executor.
getScriptExecutor()->loadState();
//Execute the onLoad event, if any.
executeScript(LevelEvent_OnLoad);
//Return true.
return true;
}
//We can't load the state so return false.
return false;
}
static std::string createNewSeed() {
static int createSeedTime = 0;
struct Buffer {
time_t systemTime;
Uint32 sdlTicks;
int x;
int y;
int createSeedTime;
} buffer;
buffer.systemTime = time(NULL);
buffer.sdlTicks = SDL_GetTicks();
SDL_GetMouseState(&buffer.x, &buffer.y);
buffer.createSeedTime = ++createSeedTime;
return Md5::toString(Md5::calc(&buffer, sizeof(buffer), NULL));
}
void Game::reset(bool save,bool noScript){
//Some sanity check, i.e. if we switch from no-script mode to script mode, we should always reset the save
assert(noScript || getScriptExecutor() || save);
//We need to reset the game so we also reset the player and the shadow.
player.reset(save);
shadow.reset(save);
saveStateNextTime=false;
loadStateNextTime=false;
//Reset the stats.
time=0;
recordings=0;
recentSwap=-10000;
if(save) recentSwapSaved=-10000;
//Reset the stats for level editor.
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->currentTime = editor->currentRecordings = -1;
editor->updateAdditionalTexture(getImageManager(), getRenderer());
}
}
//Reset the pseudo-random number generator by creating a new seed, unless we are playing from record.
if (levelFile == "?record?" || interlevel) {
if (prngSeed.empty()) {
cout << "WARNING: The record file doesn't provide a random seed! Will use a default random seed! "
"This may breaks the behavior pseudo-random number generator in script!" << endl;
prngSeed = std::string(32, '0');
} else {
#ifdef _DEBUG
cout << "Use existing PRNG seed: " << prngSeed << endl;
#endif
}
} else {
prngSeed = createNewSeed();
#ifdef _DEBUG
cout << "Create new PRNG seed: " << prngSeed << endl;
#endif
}
{
std::seed_seq seq(prngSeed.begin(), prngSeed.end());
prng.seed(seq);
}
if (save) {
prngSaved = prng;
prngSeedSaved = prngSeed;
}
//Reset the level size.
levelRect = levelRectInitial;
if (save) levelRectSaved = levelRectInitial;
//Reset the camera.
cameraMode=CAMERA_PLAYER;
if(save) cameraModeSaved=CAMERA_PLAYER;
cameraTarget = SDL_Rect{ 0, 0, 0, 0 };
if (save) cameraTargetSaved = SDL_Rect{ 0, 0, 0, 0 };
//Reset the number of collectables
currentCollectables = currentCollectablesInitial;
totalCollectables = totalCollectablesInitial;
if (save) {
currentCollectablesSaved = currentCollectablesInitial;
totalCollectablesSaved = totalCollectablesInitial;
}
//Clear the event queue, since all the events are from before the reset.
eventQueue.clear();
//Reset states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->resetAnimation(save);
}
//Also reset the background animation, if any.
if(background)
background->resetAnimation(save);
//Reset the cached notification block
notificationTexture.update(NULL, NULL);
//Reset the script environment if necessary.
if (noScript) {
//Destroys the script environment completely.
scriptExecutor->destroy();
//Clear the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
//Clear the block script.
for (auto block : levelObjects){
block->compiledScripts.clear();
}
for (auto block : levelObjectsSave){
block->compiledScripts.clear();
}
for (auto block : levelObjectsInitial){
block->compiledScripts.clear();
}
} else if (save) {
//Create a new script environment.
getScriptExecutor()->reset(true);
//Recompile the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
for (auto it = scripts.begin(); it != scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
compiledScripts[it->first] = index;
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
savedCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
initialCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
}
//Check if <levelname>.lua is present.
{
std::string s = scriptFile;
if (s.empty() && !levelFile.empty() && levelFile[0] != '?') {
s = levelFile;
size_t lp = s.find_last_of('.');
if (lp != std::string::npos) {
s = s.substr(0, lp);
}
s += ".lua";
}
int index = LUA_REFNIL;
//Now compile the file.
if (!s.empty()) {
if (s[0] == '?') {
index = getScriptExecutor()->compileScript(s.substr(1));
} else {
FILE *f = fopen(s.c_str(), "r");
if (f) {
fclose(f);
index = getScriptExecutor()->compileFile(s);
}
}
}
if (index != LUA_REFNIL) {
compiledScripts[LevelEvent_BeforeCreate] = index;
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
savedCompiledScripts[LevelEvent_BeforeCreate] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
initialCompiledScripts[LevelEvent_BeforeCreate] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
}
}
//Recompile the block script.
for (auto block : levelObjects){
block->compiledScripts.clear();
}
for (auto block : levelObjectsSave){
block->compiledScripts.clear();
}
for (auto block : levelObjectsInitial) {
block->compiledScripts.clear();
for (auto it = block->scripts.begin(); it != block->scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
block->compiledScripts[it->first] = index;
}
}
} else {
assert(getScriptExecutor());
//Do a soft reset.
getScriptExecutor()->reset(false);
//Restore the level script to initial state.
copyCompiledScripts(getScriptExecutor()->getLuaState(), initialCompiledScripts, compiledScripts);
//NOTE: We don't need to restore the block script since it will be restored automatically when the block array is copied.
}
//We reset levelObjects here since we need to wait the compiledScripts being initialized.
copyLevelObjects(levelObjectsInitial, levelObjects, true);
if (save) {
copyLevelObjects(levelObjectsInitial, levelObjectsSave, false);
}
//Also reset the last checkpoint so set it to NULL.
if (save)
objLastCheckPoint = NULL;
//Send GameObjectEvent_OnCreate event to the script
for (auto block : levelObjects) {
block->onEvent(GameObjectEvent_OnCreate);
}
//Call the level's onCreate event.
executeScript(LevelEvent_BeforeCreate);
executeScript(LevelEvent_OnCreate);
//Close exit(s) if there are any collectables
if (totalCollectables>0){
for (auto block : levelObjects){
if (block->type == TYPE_EXIT){
block->onEvent(GameObjectEvent_OnSwitchOff);
}
}
}
//Hide the cursor (if not the leveleditor).
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
void Game::executeScript(int eventType){
map<int,int>::iterator it;
//Check if there's a script for the given event.
it=compiledScripts.find(eventType);
if(it!=compiledScripts.end()){
//There is one so execute it.
getScriptExecutor()->executeScript(it->second);
}
}
void Game::broadcastObjectEvent(int eventType,int objectType,const char* id,GameObject* target){
//Create a typeGameObjectEvent that can be put into the queue.
typeGameObjectEvent e;
//Set the event type.
e.eventType=eventType;
//Set the object type.
e.objectType=objectType;
//By default flags=0.
e.flags=0;
//Unless there's an id.
if(id){
//Set flags to 0x1 and set the id.
e.flags|=1;
e.id=id;
}
//Or there's a target given.
if(target)
e.target=target;
else
e.target=NULL;
//Add the event to the queue.
eventQueue.push_back(e);
}
void Game::getCurrentLevelAutoSaveRecordPath(std::string &bestTimeFilePath,std::string &bestRecordingFilePath,bool createPath){
levels->getLevelAutoSaveRecordPath(-1,bestTimeFilePath,bestRecordingFilePath,createPath);
}
void Game::gotoNextLevel(ImageManager& imageManager, SDL_Renderer& renderer){
//Goto the next level.
levels->nextLevel();
//Check if the level exists.
if(levels->getCurrentLevel()<levels->getLevelCount()){
setNextState(STATE_GAME);
}else{
if(!levels->congratulationText.empty()){
msgBox(imageManager,renderer,_CC(levels->getDictionaryManager(),levels->congratulationText),MsgBoxOKOnly,_("Congratulations"));
}else{
msgBox(imageManager,renderer,_("You have finished the levelpack!"),MsgBoxOKOnly,_("Congratulations"));
}
//Now go back to the levelselect screen.
setNextState(STATE_LEVEL_SELECT);
//And set the music back to menu.
getMusicManager()->playMusic("menu");
}
}
void Game::GUIEventCallback_OnEvent(ImageManager& imageManager,SDL_Renderer& renderer, string name,GUIObject* obj,int eventType){
if(name=="cmdMenu"){
setNextState(STATE_LEVEL_SELECT);
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}else if(name=="cmdRestart"){
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
interlevel=false;
//And reset the game.
//NOTE: We don't need to clear the save game because in level replay the game won't be saved (??)
//TODO: it seems work (??); I'll see if it introduce bugs
reset(false, false);
}else if(name=="cmdNext"){
//No matter what, clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//And goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
void Game::invalidateNotificationTexture(Block *block) {
if (block == NULL || block == notificationTexture.getId()) {
notificationTexture.update(NULL, NULL);
}
}
diff --git a/src/Game.h b/src/Game.h
index 2194976..4025a3f 100644
--- a/src/Game.h
+++ b/src/Game.h
@@ -1,344 +1,393 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef GAME_H
#define GAME_H
#include <SDL.h>
#include <array>
#include <vector>
#include <map>
#include <string>
#include <random>
#include "CachedTexture.h"
#include "GameState.h"
#include "GUIObject.h"
#include "Block.h"
#include "Scenery.h"
#include "SceneryLayer.h"
#include "Player.h"
#include "Render.h"
#include "Shadow.h"
//This structure contains variables that make a GameObjectEvent.
struct typeGameObjectEvent{
//The type of event.
int eventType;
//The type of object that should react to the event.
int objectType;
//Flags, 0x1 means use the id.
int flags;
//Blocks with this id should react to the event.
std::string id;
//Optional pointer to the block the event should be called on.
GameObject* target;
};
class ThemeManager;
class ThemeBackground;
class TreeStorageNode;
class ScriptExecutor;
//The different level events.
enum LevelEventType{
//Event called when the level is created or the game is reset. This happens after all the blocks are created and their onCreate is called.
LevelEvent_OnCreate=1,
//Event called when the game is saved.
LevelEvent_OnSave,
//Event called when the game is loaded.
LevelEvent_OnLoad,
//Same as LevelEvent_OnCreate, but this is for the script saved in <levelname>.lua
LevelEvent_BeforeCreate = 101,
};
-class Game : public GameState,public GUIEventCallback{
+struct GameOnlySaveState {
+ //The level rect.
+ //NOTE: the x,y of these rects can only be changed by script.
+ //If not changed by script, they are always 0,0.
+ SDL_Rect levelRectSaved;
+
+ //The pseudo-random number generator which is mainly used in script.
+ std::mt19937 prngSaved;
+
+ //The seed of the pseudo-random number generator, which will be saved to and load from replay.
+ std::string prngSeedSaved;
+
+ //Integer containing the stored value of time.
+ int timeSaved;
+
+ //Integer containing the stored value of recordings.
+ int recordingsSaved;
+
+ //Integer keeping track of currently obtained collectibles
+ int currentCollectablesSaved;
+
+ //Integer keeping track of total collectibles in the level
+ int totalCollectablesSaved;
+
+ //Time of recent swap, for achievements. (in game-ticks)
+ int recentSwapSaved;
+
+ //The saved cameraMode.
+ int cameraModeSaved;
+
+ //The saved cameraTarget.
+ SDL_Rect cameraTargetSaved;
+
+ //Compiled scripts. Use lua_rawgeti(L, LUA_REGISTRYINDEX, r) to get the function.
+ std::map<int, int> savedCompiledScripts;
+
+ //Vector containing all the levelObjects in the current game.
+ std::vector<Block*> levelObjectsSave;
+};
+
+struct GameSaveState {
+ //The game save state.
+ GameOnlySaveState gameSaved;
+
+ //The player and shadow save state.
+ PlayerSaveState playerSaved, shadowSaved;
+};
+
+class Game : public GameState, public GUIEventCallback, protected GameOnlySaveState {
private:
//Boolean if the game should reset. This happens when player press 'R' button.
bool isReset;
//The script executor.
ScriptExecutor *scriptExecutor;
protected:
//contains currently played level.
TreeStorageNode* currentLevelNode;
//"tooltips" for level names, etc.
//It will be shown in the topleft corner of the screen.
TexturePtr bmTipsLevelName, bmTipsShadowDeath, bmTipsRestart, bmTipsRestartCheckpoint;
//Texture containing the action images (record, play, etc..)
SharedTexture action;
//Texture containing the medal image.
SharedTexture medals;
//ThemeBlockInstance containing the collectable image.
ThemeBlockInstance collectable;
//The name of the current level.
std::string levelName;
//The path + file of the current level.
std::string levelFile;
//The path + file of the external script file.
//If it's empty then load the levelFile with extension replaced by ".lua".
//If starts with "?" then load from string.
//Only used when the scripts are recompiled in reset() function.
std::string scriptFile;
//Editor data containing information like name, size, etc...
std::map<std::string,std::string> editorData;
//Vector used to queue the gameObjectEvents.
std::vector<typeGameObjectEvent> eventQueue;
//The themeManager.
ThemeManager* customTheme;
//The themeBackground.
ThemeBackground* background;
//Texture containing the label of the number of collectibles.
CachedTexture<int> collectablesTexture;
//Texture containing the label of the number of recordings (mainly used in level editor).
CachedTexture<int> recordingsTexture;
//Texture containing the label of the time (mainly used in level editor).
CachedTexture<int> timeTexture;
//Texture containing the notification of message block.
CachedTexture<Block*> notificationTexture;
//Texture containing the notification of interactive block.
CachedTexture<std::string> gameTipTexture;
//Texture containing the target time, etc. which is only used in level editor.
SharedTexture additionalTexture;
//Load a level from node.
//After calling this function the ownership of
//node will transfer to Game class. So don't delete
//the node after calling this function!
virtual void loadLevelFromNode(ImageManager& imageManager, SDL_Renderer& renderer, TreeStorageNode* obj, const std::string& fileName, const std::string& scriptFileName);
//(internal function) Reload the music according to level music and level pack music.
//This is mainly used in level editor.
void reloadMusic();
public:
//Array used to convert GameObject type->string.
static const char* blockName[TYPE_MAX];
//Map used to convert GameObject string->type.
static std::map<std::string,int> blockNameMap;
//Map used to convert GameObjectEventType type->string.
static std::map<int,std::string> gameObjectEventTypeMap;
//Map used to convert GameObjectEventType string->type.
static std::map<std::string,int> gameObjectEventNameMap;
//Map used to convert LevelEventType type->string.
static std::map<int,std::string> levelEventTypeMap;
//Map used to convert LevelEventType string->type.
static std::map<std::string,int> levelEventNameMap;
//The level rect.
//NOTE: the x,y of these rects can only be changed by script.
//If not changed by script, they are always 0,0.
- SDL_Rect levelRect, levelRectSaved, levelRectInitial;
+ SDL_Rect levelRect, levelRectInitial;
//The pseudo-random number generator which is mainly used in script.
- std::mt19937 prng, prngSaved;
+ std::mt19937 prng;
//The seed of the pseudo-random number generator, which will be saved to and load from replay.
- std::string prngSeed, prngSeedSaved;
+ std::string prngSeed;
//Boolean that is set to true if the level is arcade mode.
bool arcade;
//Boolean that is set to true when a game is won.
bool won;
//Boolean that is set to true when we should save game on next logic update.
bool saveStateNextTime;
//Boolean that is set to true when we should load game on next logic update.
bool loadStateNextTime;
//Boolean if the replaying currently done is for the interlevel screen.
bool interlevel;
//String containing the current tip shown in top left corner.
std::string gameTipText;
//Integer containing the number of ticks passed since the start of the level.
int time;
- //Integer containing the stored value of time.
- int timeSaved;
//Integer containing the number of recordings it took to finish.
int recordings;
- //Integer containing the stored value of recordings.
- int recordingsSaved;
//Integer keeping track of currently obtained collectibles
- int currentCollectables, currentCollectablesSaved, currentCollectablesInitial;
+ int currentCollectables, currentCollectablesInitial;
//Integer keeping track of total collectibles in the level
- int totalCollectables, totalCollectablesSaved, totalCollectablesInitial;
+ int totalCollectables, totalCollectablesInitial;
//Time of recent swap, for achievements. (in game-ticks)
- int recentSwap,recentSwapSaved;
+ int recentSwap;
//Store time of recent save/load for achievements (in millisecond)
Uint32 recentLoad,recentSave;
//Enumeration with the different camera modes.
enum CameraMode{
CAMERA_PLAYER,
CAMERA_SHADOW,
CAMERA_CUSTOM
};
//The current camera mode.
CameraMode cameraMode;
//Rectangle containing the target for the camera.
SDL_Rect cameraTarget;
- //The saved cameraMode.
- CameraMode cameraModeSaved;
- SDL_Rect cameraTargetSaved;
-
//Level scripts.
std::map<int,std::string> scripts;
//Compiled scripts. Use lua_rawgeti(L, LUA_REGISTRYINDEX, r) to get the function.
- std::map<int, int> compiledScripts, savedCompiledScripts, initialCompiledScripts;
+ std::map<int, int> compiledScripts, initialCompiledScripts;
//Vector containing all the levelObjects in the current game.
- std::vector<Block*> levelObjects, levelObjectsSave, levelObjectsInitial;
+ std::vector<Block*> levelObjects, levelObjectsInitial;
//The layers for the scenery.
//
// We utilize the fact that std::map is sorted, and we compare the layer name with "f",
// If name<"f" then it's background layer, if name>="f" then it's foreground layer.
//
// However, we hide this complexity from users, so when creating/editing layers,
// the user is asked to choose whether the layer is foreground.
//
// In fact the background layer is always named "bg_"+actual name of the layer
// and the foreground layer is always named "fg_"+actual name of the layer
std::map<std::string, SceneryLayer*> sceneryLayers;
//The player...
Player player;
//... and his shadow.
Shadow shadow;
//warning: weak reference only, may point to invalid location
Block::ObservePointer objLastCheckPoint;
//Constructor.
Game(SDL_Renderer& renderer, ImageManager& imageManager);
//If this is not empty then when next Game class is created
//it will play this record file.
static std::string recordFile;
//Destructor.
//It will call destroy();
~Game();
//Method used to clean up the GameState.
void destroy();
//Inherited from GameState.
void handleEvents(ImageManager&imageManager, SDL_Renderer&renderer) override;
void logic(ImageManager& imageManager, SDL_Renderer& renderer) override;
void render(ImageManager&,SDL_Renderer& renderer) override;
void resize(ImageManager& imageManager, SDL_Renderer& renderer) override;
+ //Check if we should save/load state.
+ //This function is called by logic(). The RecordPlayback override it to provide seeking support.
+ virtual void checkSaveLoadState();
+
//This method will load a level.
//fileName: The fileName of the level.
virtual void loadLevel(ImageManager& imageManager, SDL_Renderer& renderer, std::string fileName);
//Method used to broadcast a GameObjectEvent.
//eventType: The type of event.
//objectType: The type of object that should react to the event.
//id: The id of the blocks that should react.
//target: Pointer to the object
void broadcastObjectEvent(int eventType,int objectType=-1,const char* id=NULL,GameObject* target=NULL);
//Compile all scripts and run onCreate script.
//NOTE: Call this function only after script state reset, or there will be some memory leaks.
void compileScript();
//Method that will check if a script for a given levelEvent is present.
//If that's the case the script will be executed.
//eventType: The level event type to execute.
void inline executeScript(int eventType);
//Returns if the player and shadow can save the current state.
bool canSaveState();
//Method used to store the current state.
//This is used for checkpoints.
//Returns: True if it succeeds without problems.
bool saveState();
//Method used to load the stored state.
//This is used for checkpoints.
//Returns: True if it succeeds without problems.
bool loadState();
//Method that will reset the GameState to it's initial state.
//save: Boolean if the saved state should also be deleted. This also means recreate the Lua context.
//noScript: Boolean if we should not compile the script at all. This is used by level editor when exiting test play.
void reset(bool save,bool noScript);
+ void saveStateInternal(GameSaveState* o);
+ void loadStateInternal(GameSaveState* o);
+ void saveGameOnlyStateInternal(GameOnlySaveState* o);
+ void loadGameOnlyStateInternal(GameOnlySaveState* o);
+
//Save current game record to the file.
//fileName: The filename of the destination file.
void saveRecord(const char* fileName);
//Method called by the player (or shadow) when he finished.
void replayPlay(ImageManager& imageManager, SDL_Renderer &renderer);
//get current level's auto-save record path,
//using current level's MD5, file name and other information.
void getCurrentLevelAutoSaveRecordPath(std::string &bestTimeFilePath,std::string &bestRecordingFilePath,bool createPath);
//Method that will prepare the gamestate for the next level and start it.
//If it's the last level it will show the congratulations text and return to the level select screen.
void gotoNextLevel(ImageManager& imageManager, SDL_Renderer& renderer);
//Get the name of the current level.
const std::string& getLevelName(){
return levelName;
}
//GUI event handling is done here.
void GUIEventCallback_OnEvent(ImageManager&imageManager, SDL_Renderer&renderer, std::string name, GUIObject* obj, int eventType) override;
//Get the script executor.
ScriptExecutor* getScriptExecutor() {
return scriptExecutor;
}
//Invalidates the notification texture.
//block: The block which is updated. The cached texture won't be invalidated if it's not for this block.
//NULL means invalidates the texture no matter which block is updated.
void invalidateNotificationTexture(Block *block = NULL);
//Translate and expand the message of a notification block.
std::string translateAndExpandMessage(const std::string &untranslated_message);
};
#endif
diff --git a/src/LevelEditor.h b/src/LevelEditor.h
index bea1f48..54e2411 100644
--- a/src/LevelEditor.h
+++ b/src/LevelEditor.h
@@ -1,443 +1,445 @@
/*
* Copyright (C) 2011-2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LEVELEDITOR_H
#define LEVELEDITOR_H
#include "CachedTexture.h"
#include "GameState.h"
#include "GameObjects.h"
#include "Player.h"
#include "Game.h"
#include "GUIObject.h"
#include <vector>
#include <map>
#include <string>
enum class ToolTips {
Select = 0,
Add,
Delete,
Play,
UndoNoTooltip, // dynamically generated
RedoNoTooltip, // dynamically generated
LevelSettings,
SaveLevel,
BackToMenu,
Select_UsedInSelectionPopup,
Delete_UsedInSelectionPopup,
Configure_UsedInSelectionPopup,
TooltipMax,
};
//Class that represents a moving position for moving blocks.
class MovingPosition{
public:
//Integer containing the relative time used to store in the level.
int time;
//The x location.
int x;
//The x location.
int y;
//Constructor.
//x: The x position relative to the moving block's position.
//y: The y position relative to the moving block's position.
//time: The time it takes from the previous position to here.
MovingPosition(int x,int y,int time);
//Destructor.
~MovingPosition();
//This will update the moving position.
//x: The x position relative to the moving block's position.
//y: The y position relative to the moving block's position.
void updatePosition(int x,int y);
};
//Internal selection popup class.
class LevelEditorSelectionPopup;
//Internal actions popup class.
class LevelEditorActionsPopup;
class CommandManager;
class AddRemoveGameObjectCommand;
class MoveGameObjectCommand;
class AddLinkCommand;
class RemoveLinkCommand;
class AddRemovePathCommand;
class RemovePathCommand;
class SetLevelPropertyCommand;
class SetScriptCommand;
class AddRemoveLayerCommand;
class SetLayerPropertyCommand;
class MoveToLayerCommand;
class SetEditorPropertyCommand;
+class ResizeLevelCommand;
class HelpManager;
//The LevelEditor state, it's based on the Game state.
class LevelEditor: public Game{
friend class Game;
friend class LevelEditorSelectionPopup;
friend class LevelEditorActionsPopup;
friend class AddRemoveGameObjectCommand;
friend class MoveGameObjectCommand;
friend class AddLinkCommand;
friend class RemoveLinkCommand;
friend class AddRemovePathCommand;
friend class RemovePathCommand;
friend class SetLevelPropertyCommand;
friend class SetScriptCommand;
friend class AddRemoveLayerCommand;
friend class SetLayerPropertyCommand;
friend class MoveToLayerCommand;
friend class SetEditorPropertyCommand;
+ friend class ResizeLevelCommand;
private:
//Boolean if the user isplaying/testing the level.
bool playMode;
//Enumaration containing the tools.
//SELECT: The select tool, for selecting/dragging blocks.
//ADD: For adding blocks.
//REMOVE: For removing blocks.
enum Tools{
SELECT,
ADD,
REMOVE,
NUMBER_TOOLS
};
//The tool the user has selected.
Tools tool;
//The toolbar surface.
SharedTexture toolbar;
//Rectangle the size and location of the toolbar on screen.
SDL_Rect toolbarRect;
//The selection popup (if any)
LevelEditorSelectionPopup* selectionPopup;
//The actions popup (if any)
LevelEditorActionsPopup* actionsPopup;
//Map used to get the GameObject that belongs to a certain GUIWindow.
map<GUIObject*,GameObject*> objectWindows;
//Map which store the visibility of each scenery layers, "" (empty) means the Block layer
map<string, bool> layerVisibility;
//The selected layer, "" (empty) means the Block layer
string selectedLayer;
//Vector containing pointers to the selected GameObjects.
vector<GameObject*> selection;
//The selection square.
SharedTexture selectionMark;
//A circle at the location of moving positions in configure mode.
SharedTexture movingMark;
//Texture showing the movement speed.
CachedTexture<int> movementSpeedTexture;
//Texture showing the pause time
CachedTexture<int> pauseTimeTexture;
//GUI image.
SharedTexture bmGUI;
//Texture containing the text "Toolbox"
TexturePtr toolboxText;
//Keeps track of commands for undo and redo.
CommandManager* commandManager;
//The current type of block to place in Add mode.
int currentType;
std::array<TexturePtr,TYPE_MAX> typeTextTextures;
std::array<TexturePtr,static_cast<size_t>(ToolTips::TooltipMax)> tooltipTextures;
CachedTexture<std::string> undoTooltipTexture;
CachedTexture<std::string> redoTooltipTexture;
std::map<std::string, TexturePtr> cachedTextTextures;
TexturePtr& getCachedTextTexture(SDL_Renderer& renderer, const std::string& text);
TexturePtr& getSmallCachedTextTexture(SDL_Renderer& renderer, const std::string& text);
//Boolean if the tool box is displayed.
bool toolboxVisible;
//The rect of tool box tip.
SDL_Rect toolboxRect;
//The first item in tool box.
int toolboxIndex;
//Boolean if the shift button is pressed.
bool pressedShift;
//Boolean if the left mouse button is pressed.
bool pressedLeftMouse;
//Boolean if the mouse is dragged. (Left button pressed and moved)
bool dragging;
//The camera x velocity.
int cameraXvel;
int cameraYvel;
//SDL_Rect used to store the camera's location when entering playMode.
SDL_Rect cameraSave;
//Integer indicating if the selection is dragged and the drag mode.
// -1: not dragged
// 4: dragged
// 0,1,2,3,5,6,7,8: resizing the dragCenter
// 012
// 3x5
// 678
int selectionDrag;
//Pointer to the gameobject that's the center of the drag.
GameObject* dragCenter;
// The drag start position which is used when dragging blocks
SDL_Point dragSrartPosition;
//Integer containing a unique id.
//Everytime a new id is needed it will increase by one.
unsigned int currentId;
typedef map<Block*, vector<GameObject*> > Triggers;
//Vector containing the trigger GameObjects.
Triggers triggers;
//Boolean used in configure mode when linking triggers with their targets.
bool linking;
//Pointer to the trigger that's is being linked.
Block* linkingTrigger;
//Vector containing the moving GameObjects.
map<Block*,vector<MovingPosition> > movingBlocks;
//Integer containing the speed the block is moving for newly added blocks. 1 movingSpeed = 0.1 pixel/frame = 0.08 block/s
//The movingSpeed is capped at 125 (10 block/s).
int movingSpeed;
//The pause time for path edit if the current point is equal to the previous time. 1 pauseTime = 1 frame = 0.04s
int pauseTime;
//Boolean used in configure mode when configuring moving blocks.
bool moving;
//Another boolean used in configure mode when configuring moving blocks.
bool pauseMode;
//Pointer to the moving block that's is being configured.
Block* movingBlock;
//Value used for placing the Movespeed label
int movingSpeedWidth;
//The clipboard.
vector<map<string,string> > clipboard;
//String containing the levelTheme.
std::string levelTheme;
//String containing the levelMusic.
std::string levelMusic;
//Integer containing the button of which a tool tip should be shown.
int tooltip;
//The target time and recordings of the current editing level.
int levelTime, levelRecordings;
//The current time and recordings of the current editing level.
int currentTime, currentRecordings;
//The best time and recordings of the current editing level.
int bestTime, bestRecordings;
//The help manager for script API document.
HelpManager *helpMgr;
//GUI event handling is done here.
void GUIEventCallback_OnEvent(ImageManager&, SDL_Renderer&, std::string name,GUIObject* obj,int eventType);
//Method for deleting a GUIWindow.
//NOTE: This function checks for the presence of the window in the GUIObjectRoot and objectWindows map.
//window: Pointer to the GUIWindow.
void destroyWindow(GUIObject* window);
//Method that will let you configure the levelSettings.
void levelSettings(ImageManager& imageManager,SDL_Renderer &renderer);
//Method used to save the level.
//fileName: Thge filename to write the level to.
void saveLevel(string fileName);
//Method used to convert a given x and y to snap to grid.
//x: Pointer to the x location.
//y: Pointer to the y location.
void snapToGrid(int* x,int* y);
//Method used to check if the cursor is near the border of screen and we should move the camera.
//This method will check if the mouse is near a screen edge.
//r: An array of SDL_Rect, does nothing if mouse inside these rectange(s).
//count: Number of rectangles.
//If so it will move the camera.
void setCamera(const SDL_Rect* r,int count);
//Just an internal function.
static std::string describeSceneryName(const std::string& name);
//Array containing translateble block names
static const char* blockNames[TYPE_MAX];
public:
//Array containing the ids of different block types in a wanted order
//Maybe also useful to disable deprecated block types in the editor
//PLEASE NOTE: Must be updated for new block types
//Ordered for Edward Liis proposal:
//Normal->Shadow->Spikes->Fragile
//Normal moving->Shadow moving->Moving spikes
//Conveyor belt->Shadow conveyor belt
//Button->Switch->Portal->Swap->Checkpoint->Notification block
//Player start->Shadow start->Exit
//Collectable->Pushable
static const int EDITOR_ORDER_MAX=20;
static const int editorTileOrder[EDITOR_ORDER_MAX];
//Get the localized block name
static std::string getLocalizedBlockName(int type);
//Array containing the names of available scenery blocks
std::vector<std::string> sceneryBlockNames;
// get the number of available blocks depending on the selected layer
int getEditorOrderMax() const {
if (selectedLayer.empty()) return EDITOR_ORDER_MAX;
return sceneryBlockNames.size() + 1; // the added one is for custom scenery block
}
protected:
//Inherits the function loadLevelFromNode from Game class.
virtual void loadLevelFromNode(ImageManager& imageManager, SDL_Renderer& renderer, TreeStorageNode* obj, const std::string& fileName, const std::string& scriptFileName) override;
public:
//Constructor.
LevelEditor(SDL_Renderer &renderer, ImageManager &imageManager);
//Destructor.
~LevelEditor();
//Method that will reset some default values.
void reset();
//Inherited from Game(State).
void handleEvents(ImageManager& imageManager, SDL_Renderer& renderer) override;
void logic(ImageManager& imageManager, SDL_Renderer& renderer) override;
void render(ImageManager& imageManager, SDL_Renderer& renderer) override;
void resize(ImageManager& imageManager, SDL_Renderer& renderer) override;
//Method used to draw the currentType on the placement surface.
//This will only be called when the tool is ADD.
void showCurrentObject(SDL_Renderer &renderer);
//Method used to draw the selection that's being dragged.
void showSelectionDrag(SDL_Renderer &renderer);
//Method used to draw configure tool specific things like moving positions, teleport lines.
void showConfigure(SDL_Renderer &renderer);
//Method that will render the HUD.
//It will be rendered after the placement suface but before the toolbar.
void renderHUD(SDL_Renderer &renderer);
//Method called after loading a level.
//It will fill the triggers vector.
void postLoad();
//Event that is invoked when there's a mouse click on an object.
//obj: Pointer to the GameObject clicked on.
//selected: Boolean if the GameObject that has been clicked on was selected.
void onClickObject(GameObject* obj,bool selected);
//Event that is invoked when there's a right mouse button click on an object.
//obj: Pointer to the GameObject clicked on.
//selected: Boolean if the GameObject that has been clicked on was selected.
void onRightClickObject(ImageManager& imageManager, SDL_Renderer& renderer, GameObject* obj, bool);
//Event that is invoked when there's a mouse click but not on any object.
//x: The x location of the click on the game field (+= camera.x).
//y: The y location of the click on the game field (+= camera.y).
void onClickVoid(int x,int y);
//Event that is invoked when there's a mouse right click but not on any object.
//x: The x location of the click on the game field (+= camera.x).
//y: The y location of the click on the game field (+= camera.y).
void onRightClickVoid(ImageManager& imageManager, SDL_Renderer& renderer, int x,int y);
//Event that is invoked when the dragging starts.
//x: The x location the drag started. (mouse.x+camera.x)
//y: The y location the drag started. (mouse.y+camera.y)
void onDragStart(int x,int y);
//Event that is invoked when the mouse is dragged.
//dx: The relative x distance the mouse dragged.
//dy: The relative y distance the mouse dragged.
void onDrag(int dx,int dy);
//Event that is invoked when the mouse stopped dragging.
//x: The x location the drag stopped. (mouse.x+camera.x)
//y: The y location the drag stopped. (mouse.y+camera.y)
void onDrop(int x,int y);
//Event that is invoked when the camera is moved.
//dx: The relative x distance the camera moved.
//dy: The relative y distance the camera moved.
void onCameraMove(int dx,int dy);
//internal function called by onClickObject() and onClickVoid().
void addMovingPosition(int x,int y);
//Set dirty of selection popup
void selectionDirty();
//Deselect all blocks
void deselectAll();
//Save current level and show a notification dialog
void saveCurrentLevel(ImageManager& imageManager, SDL_Renderer& renderer);
// An internal function to determine new position in drag mode.
// Make sure selectionDrag=4 when calling this function.
void determineNewPosition(int& x, int& y);
// An internal function to determine new size in resize mode.
// Make sure selectionDrag=0,1,2,3,5,6,7,8 when calling this function.
void determineNewSize(int x, int y, SDL_Rect& r);
//Call this function to start test play.
void enterPlayMode(ImageManager& imageManager, SDL_Renderer& renderer);
//Update the additional texture displayed in test play.
void updateAdditionalTexture(ImageManager& imageManager, SDL_Renderer& renderer);
//Update the record in play mode.
void updateRecordInPlayMode(ImageManager& imageManager, SDL_Renderer& renderer);
void undo();
void redo();
//Get the GUI texture.
inline SharedTexture& getGuiTexture() {
return bmGUI;
}
//Get the play mode.
bool isPlayMode() const {
return playMode;
}
};
#endif
diff --git a/src/Player.cpp b/src/Player.cpp
index 9873687..4c0aa52 100644
--- a/src/Player.cpp
+++ b/src/Player.cpp
@@ -1,1743 +1,1751 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Block.h"
#include "Player.h"
#include "Game.h"
#include "Functions.h"
#include "Settings.h"
#include "FileManager.h"
#include "Globals.h"
#include "InputManager.h"
#include "SoundManager.h"
#include "StatisticsManager.h"
#include "MD5.h"
#include <stdio.h>
#include <iostream>
#include <SDL.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
#ifdef RECORD_FILE_DEBUG
string recordKeyPressLog,recordKeyPressLog_saved;
vector<SDL_Rect> recordPlayerPosition,recordPlayerPosition_saved;
#endif
//static internal array to store time of recent deaths for achievements
static Uint32 recentDeaths[10]={0};
static int loadAndDieTimes=0;
//static internal function to add recent deaths and update achievements
static inline void addRecentDeaths(Uint32 recentLoad){
//Get current time in ms.
//We added it by 5 seconds to avoid bug if you choose a level to play
//and die in 5 seconds after the game has startup.
Uint32 t=SDL_GetTicks()+5000;
for(int i=9;i>0;i--){
recentDeaths[i]=recentDeaths[i-1];
}
recentDeaths[0]=t;
//Update achievements
if(recentDeaths[4]+5000>t){
statsMgr.newAchievement("die5in5");
}
if(recentDeaths[9]+5000>t){
statsMgr.newAchievement("die10in5");
}
if(recentLoad+1000>t){
statsMgr.newAchievement("loadAndDie");
}
}
-Player::Player(Game* objParent):xVelBase(0),yVelBase(0),objParent(objParent),recordSaved(false),
-inAirSaved(false),isJumpSaved(false),canMoveSaved(false),holdingOtherSaved(false){
+Player::Player(Game* objParent):xVelBase(0),yVelBase(0),objParent(objParent){
//Set the dimensions of the player.
//The size of the player is 21x40.
box.x=0;
box.y=0;
box.w=23;
box.h=40;
//Set his velocity to zero.
xVel=0;
yVel=0;
//Set the start position.
fx=0;
fy=0;
//Set some default values.
- inAir=true;
- isJump=false;
+ inAir = inAirSaved = true;
+ isJump = isJumpSaved = false;
shadowCall=false;
shadow=false;
- canMove=true;
- holdingOther=false;
+ canMove = canMoveSaved = true;
+ holdingOther = holdingOtherSaved = false;
dead=false;
- record=false;
+ record = recordSaved = false;
downKeyPressed=false;
spaceKeyPressed=false;
recordIndex=-1;
#ifdef RECORD_FILE_DEBUG
recordKeyPressLog.clear();
recordKeyPressLog_saved.clear();
recordPlayerPosition.clear();
recordPlayerPosition_saved.clear();
#endif
//Some default values for animation variables.
direction=0;
jumpTime=0;
state=stateSaved=0;
//xVelSaved is used to store if there's a state saved or not.
xVelSaved=yVelSaved=0x80000000;
objCurrentStand = objLastStand = objLastTeleport = objNotificationBlock = objShadowBlock = NULL;
objCurrentStandSave = objLastStandSave = objLastTeleportSave = objNotificationBlockSave = objShadowBlockSave = NULL;
}
Player::~Player(){
//Do nothing here
}
bool Player::isPlayFromRecord(){
return recordIndex>=0; // && recordIndex<(int)recordButton.size();
}
//get the game record object.
std::vector<int>* Player::getRecord(){
return &recordButton;
}
#ifdef RECORD_FILE_DEBUG
string& Player::keyPressLog(){
return recordKeyPressLog;
}
vector<SDL_Rect>& Player::playerPosition(){
return recordPlayerPosition;
}
#endif
//play the record.
void Player::playRecord(){
recordIndex=0;
}
void Player::spaceKeyDown(class Shadow* shadow){
//Start recording or stop, depending on the recording state.
if(record==false){
//We start recording.
if(shadow->called==true){
//The shadow is still busy so first stop him before we can start recording.
shadowCall=false;
shadow->called=false;
shadow->playerButton.clear();
}else if(!dead){
//Check if shadow is dead.
if(shadow->dead){
//Show tooltip.
//Just reset the countdown (the shadow's jumptime).
shadow->jumpTime=80;
//Play the error sound.
getSoundManager()->playSound("error");
}else{
//The shadow isn't moving and both player and shadow aren't dead so start recording.
record=true;
//We start a recording meaning we need to increase recordings by one.
objParent->recordings++;
//Update statistics.
if(!dead && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
statsMgr.recordTimes++;
if(statsMgr.recordTimes==100) statsMgr.newAchievement("record100");
if(statsMgr.recordTimes==1000) statsMgr.newAchievement("record1k");
}
}
}
}else{
//The player is recording so stop recording and call the shadow.
record=false;
shadowCall=true;
}
}
void Player::handleInput(class Shadow* shadow){
//Check if we should read the input from record file.
//Actually, we read input from record file in
//another function shadowSetState.
bool readFromRecord=false;
if(recordIndex>=0 && recordIndex<(int)recordButton.size()) readFromRecord=true;
if(!readFromRecord){
//Reset horizontal velocity.
xVel=0;
if(inputMgr.isKeyDown(INPUTMGR_RIGHT)){
//Walking to the right.
xVel+=7;
}
if(inputMgr.isKeyDown(INPUTMGR_LEFT)){
//Walking to the left.
if(xVel!=0 && !dead && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
//Horizontal confusion achievement :)
statsMgr.newAchievement("horizontal");
}
xVel-=7;
}
//Check if the action key has been released.
if(!inputMgr.isKeyDown(INPUTMGR_ACTION)){
//It has so downKeyPressed can't be true.
downKeyPressed=false;
}
/*
//Don't reset spaceKeyPressed or when you press the space key
//and release another key then the bug occurs. (ticket #44)
if(event.type==SDL_KEYUP || !inputMgr.isKeyDown(INPUTMGR_SPACE)){
spaceKeyPressed=false;
}*/
}
//Check if a key is pressed (down).
if(inputMgr.isKeyDownEvent(INPUTMGR_JUMP) && !readFromRecord){
//The up key, if we aren't in the air we start jumping.
//Fixed a potential bug
if(!inAir && !isJump){
#ifdef RECORD_FILE_DEBUG
char c[64];
sprintf(c,"[%05d] Jump key pressed\n",objParent->time);
cout<<c;
recordKeyPressLog+=c;
#endif
isJump=true;
}
}else if(inputMgr.isKeyDownEvent(INPUTMGR_SPACE) && !readFromRecord){
//Fixed a potential bug
if(!spaceKeyPressed){
#ifdef RECORD_FILE_DEBUG
char c[64];
sprintf(c,"[%05d] Space key pressed\n",objParent->time);
cout<<c;
recordKeyPressLog+=c;
#endif
spaceKeyDown(shadow);
spaceKeyPressed=true;
}
}else if(inputMgr.isKeyUpEvent(INPUTMGR_SPACE) && !readFromRecord){
if(record && getSettings()->getBoolValue("quickrecord")){
spaceKeyDown(shadow);
spaceKeyPressed=true;
}
}else if(record && !readFromRecord && inputMgr.isKeyDownEvent(INPUTMGR_CANCELRECORDING)){
//Cancel current recording
//Search the recorded button and clear the last space key press
int i=recordButton.size()-1;
for(;i>=0;i--){
if(recordButton[i] & PlayerButtonSpace){
recordButton[i] &= ~PlayerButtonSpace;
break;
}
}
if(i>=0){
//Clear the recording at the player's side.
playerButton.clear();
line.clear();
//reset the record flag
record=false;
//decrese the record count
objParent->recordings--;
}else{
cout<<"Failed to find last recording"<<endl;
}
}else if(inputMgr.isKeyDownEvent(INPUTMGR_ACTION)){
//Downkey is pressed.
//Fixed a potential bug
if(!downKeyPressed){
#ifdef RECORD_FILE_DEBUG
char c[64];
sprintf(c,"[%05d] Action key pressed\n",objParent->time);
cout<<c;
recordKeyPressLog+=c;
#endif
downKeyPressed=true;
}
}else if(inputMgr.isKeyDownEvent(INPUTMGR_SAVE)){
//F2 only works in the level editor.
if(!(dead || shadow->dead) && stateID==STATE_LEVEL_EDITOR){
//Save the state. (delayed)
if (objParent && !objParent->player.isPlayFromRecord() && !objParent->interlevel)
objParent->saveStateNextTime=true;
}
}else if(inputMgr.isKeyDownEvent(INPUTMGR_LOAD) && (!readFromRecord || objParent->interlevel)){
//F3 is used to load the last state.
if (objParent && canLoadState()) {
recordIndex = -1;
objParent->loadStateNextTime = true;
//Also delete any gui (most likely the interlevel gui). Only in game mode.
if (GUIObjectRoot && stateID != STATE_LEVEL_EDITOR){
delete GUIObjectRoot;
GUIObjectRoot = NULL;
}
//And set interlevel to false.
objParent->interlevel = false;
}
}else if(inputMgr.isKeyDownEvent(INPUTMGR_SWAP)){
//F4 will swap the player and the shadow, but only in the level editor.
if(!(dead || shadow->dead) && stateID==STATE_LEVEL_EDITOR){
swapState(shadow);
}
}else if(inputMgr.isKeyDownEvent(INPUTMGR_TELEPORT)){
//F5 will revive and teleoprt the player to the cursor. Only works in the level editor.
//Shift+F5 teleports the shadow.
if(stateID==STATE_LEVEL_EDITOR){
//get the position of the cursor.
int x,y;
SDL_GetMouseState(&x,&y);
x+=camera.x;
y+=camera.y;
if(inputMgr.isKeyDown(INPUTMGR_SHIFT)){
//teleports the shadow.
shadow->dead=false;
shadow->box.x=x;
shadow->box.y=y;
}else{
//teleports the player.
dead=false;
box.x=x;
box.y=y;
}
//play sound?
getSoundManager()->playSound("swap");
}
}else if(inputMgr.isKeyDownEvent(INPUTMGR_SUICIDE)){
//F12 is suicide and only works in the leveleditor.
if(stateID==STATE_LEVEL_EDITOR){
die();
shadow->die();
}
}
}
void Player::setLocation(int x,int y){
box.x=x;
box.y=y;
}
void Player::move(vector<Block*> &levelObjects,int lastX,int lastY){
//Only move when the player isn't dead.
//Fixed the bug that player/shadow can teleport or pull the switch even if died.
//FIXME: Don't know if there will be any side-effects.
if(dead) return;
//Pointer to a checkpoint.
Block* objCheckPoint=NULL;
//Pointer to a swap.
Block* objSwap=NULL;
//Set the objShadowBlock to NULL.
//Only for swapping to prevent the shadow from swapping in a shadow block.
objShadowBlock=NULL;
//Set the objNotificationBlock to NULL.
objNotificationBlock=NULL;
//NOTE: to fix bugs regarding player/shadow swap, we should first process collision of player/shadow
//then move them. The code is moved to Game::logic().
/*//Store the location.
int lastX=box.x;
int lastY=box.y;
collision(levelObjects);*/
bool canTeleport=true;
bool isTraveling=true;
// for checking the achievenemt that player and shadow come to exit simultaneously.
bool weWon = false;
//Now check the functional blocks.
for(unsigned int o=0;o<levelObjects.size();o++){
//Skip block which is not visible.
if (levelObjects[o]->queryProperties(GameObjectProperty_Flags, this) & 0x80000000) continue;
//Check for collision.
if(checkCollision(box,levelObjects[o]->getBox())){
//Now switch the type.
switch(levelObjects[o]->type){
case TYPE_CHECKPOINT:
{
//If we're not the shadow set the gameTip to Checkpoint.
if (!shadow && objParent != NULL) {
objParent->gameTipText += tfm::format(
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
_("Press %s key to save the game."),
InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_ACTION, false), inputMgr.getKeyCode(INPUTMGR_ACTION, true))) + "\n";
}
//And let objCheckPoint point to this object.
objCheckPoint=levelObjects[o];
break;
}
case TYPE_SWAP:
{
//If we're not the shadow set the gameTip to swap.
if (!shadow && objParent != NULL) {
objParent->gameTipText += tfm::format(
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
_("Press %s key to swap the position of player and shadow."),
InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_ACTION, false), inputMgr.getKeyCode(INPUTMGR_ACTION, true))) + "\n";
}
//And let objSwap point to this object.
objSwap=levelObjects[o];
break;
}
case TYPE_EXIT:
{
//Check to see if we have enough keys to finish the level
if(objParent->currentCollectables>=objParent->totalCollectables){
//Update achievements if we're not in the leveleditor.
if (stateID != STATE_LEVEL_EDITOR && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
if(objParent->player.dead || objParent->shadow.dead){
//Finish the level with player or shadow died.
statsMgr.newAchievement("forget");
}
if(objParent->won && !weWon){ // This checks if somebody already hit the exit but we haven't hit the exit yet.
//Player and shadow come to exit simultaneously.
statsMgr.newAchievement("jit");
}
}
//We can't just handle the winning here (in the middle of the update cycle)/
//So set won in Game true.
objParent->won=true;
//We hit the exit.
weWon = true;
}
break;
}
case TYPE_PORTAL:
{
//Check if the teleport id isn't empty.
if(levelObjects[o]->id.empty()){
std::cerr<<"WARNING: Invalid teleport id!"<<std::endl;
canTeleport=false;
}
//If we're not the shadow set the gameTip to portal.
if (!shadow && objParent != NULL) {
if (levelObjects[o]->message.empty()) {
objParent->gameTipText += tfm::format(
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
_("Press %s key to teleport."),
InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_ACTION, false), inputMgr.getKeyCode(INPUTMGR_ACTION, true))) + "\n";
} else {
objParent->gameTipText += objParent->translateAndExpandMessage(levelObjects[o]->message) + "\n";
}
}
//Check if we can teleport and should (downkey -or- auto).
if(canTeleport && (downKeyPressed || (levelObjects[o]->queryProperties(GameObjectProperty_Flags,this)&1))){
canTeleport=false;
if(downKeyPressed || levelObjects[o]!=objLastTeleport.get()){
//Loop the levelobjects again to find the destination.
for(unsigned int oo=o+1;;){
//We started at our index+1.
//Meaning that if we reach the end of the vector then we need to start at the beginning.
if(oo>=levelObjects.size())
oo-=(int)levelObjects.size();
//It also means that if we reach the same index we need to stop.
//If the for loop breaks this way then we have no succes.
if(oo==o){
//Couldn't teleport. We play the error sound only when the down key pressed.
if (downKeyPressed) {
getSoundManager()->playSound("error");
}
break;
}
//Check if the second (oo) object is a portal and is visible.
if (levelObjects[oo]->type == TYPE_PORTAL && (levelObjects[oo]->queryProperties(GameObjectProperty_Flags, this) & 0x80000000) == 0){
//Check the id against the destination of the first portal.
if(levelObjects[o]->destination==levelObjects[oo]->id){
//Get the destination location.
SDL_Rect r = levelObjects[oo]->getBox();
r.x += 5;
r.y += 2;
r.w = box.w;
r.h = box.h;
//Check if the destination location is blocked.
bool blocked = false;
for (auto ooo : levelObjects){
//Make sure to only check visible blocks.
if (ooo->queryProperties(GameObjectProperty_Flags, this) & 0x80000000)
continue;
//Make sure the object is solid for the player.
if (!ooo->queryProperties(GameObjectProperty_PlayerCanWalkOn, this))
continue;
//Check for collision.
if (checkCollision(r, ooo->getBox())) {
blocked = true;
break;
}
}
//Teleport only if the destination is not blocked.
if (!blocked) {
//Call the event.
objParent->broadcastObjectEvent(GameObjectEvent_OnToggle, -1, NULL, levelObjects[o]);
objLastTeleport = levelObjects[oo];
//Teleport the player.
box.x = r.x;
box.y = r.y;
//We don't count it to traveling distance.
isTraveling = false;
//Play the swap sound.
getSoundManager()->playSound("swap");
break;
}
}
}
//Increase oo.
oo++;
}
//Reset the down key pressed.
downKeyPressed = false;
}
}
break;
}
case TYPE_SWITCH:
{
//If we're not the shadow set the gameTip to switch.
if (!shadow && objParent != NULL) {
if (levelObjects[o]->message.empty()) {
objParent->gameTipText += tfm::format(
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
_("Press %s key to activate the switch."),
InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_ACTION, false), inputMgr.getKeyCode(INPUTMGR_ACTION, true))) + "\n";
} else {
objParent->gameTipText += objParent->translateAndExpandMessage(levelObjects[o]->message) + "\n";
}
}
//If the down key is pressed then invoke an event.
if(downKeyPressed){
//Play the animation.
levelObjects[o]->playAnimation();
//Play the toggle sound.
getSoundManager()->playSound("toggle");
//Update statistics.
if(!dead && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
statsMgr.switchTimes++;
//Update achievements
switch(statsMgr.switchTimes){
case 100:
statsMgr.newAchievement("switch100");
break;
case 1000:
statsMgr.newAchievement("switch1k");
break;
}
}
levelObjects[o]->onEvent(GameObjectEvent_OnPlayerInteraction);
}
break;
}
case TYPE_SHADOW_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
{
//This only applies to the player.
if(!shadow)
objShadowBlock=levelObjects[o];
break;
}
case TYPE_NOTIFICATION_BLOCK:
{
//This only applies to the player.
if(!shadow)
objNotificationBlock=levelObjects[o];
break;
}
case TYPE_COLLECTABLE:
{
//Check if collectable is active (if it's not it's equal to 1(inactive))
if((levelObjects[o]->queryProperties(GameObjectProperty_Flags, this) & 0x1) == 0) {
//Toggle an event
objParent->broadcastObjectEvent(GameObjectEvent_OnToggle,-1,NULL,levelObjects[o]);
//Increase the current number of collectables
objParent->currentCollectables++;
getSoundManager()->playSound("collect");
//Open exit(s)
if(objParent->currentCollectables>=objParent->totalCollectables){
for(unsigned int i=0;i<levelObjects.size();i++){
if(levelObjects[i]->type==TYPE_EXIT){
objParent->broadcastObjectEvent(GameObjectEvent_OnSwitchOn,-1,NULL,levelObjects[i]);
}
}
}
}
break;
}
}
//Now check for the spike property.
if(levelObjects[o]->queryProperties(GameObjectProperty_IsSpikes,this)){
//It is so get the collision box.
SDL_Rect r=levelObjects[o]->getBox();
//TODO: pixel-accuracy hit test.
//For now we shrink the box.
r.x+=2;
r.y+=2;
r.w-=4;
r.h-=4;
//Check collision, if the player collides then let him die.
if(checkCollision(box,r)){
die();
}
}
}
}
//Check if the player can teleport.
if(canTeleport)
objLastTeleport=NULL;
//Check the checkpoint pointer only if the downkey is pressed.
//new: don't save the game if playing game record
if (objParent != NULL && downKeyPressed && objCheckPoint != NULL && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
//Checkpoint thus save the state.
if(objParent->canSaveState()){
objParent->saveStateNextTime=true;
objParent->objLastCheckPoint=objCheckPoint;
}
}
//Check the swap pointer only if the down key is pressed.
if(objSwap!=NULL && downKeyPressed && objParent!=NULL){
//Now check if the shadow we're the shadow or not.
if(shadow){
if(!(dead || objParent->player.dead)){
//Check if the player isn't in front of a shadow block.
if(!objParent->player.objShadowBlock.get()){
objParent->player.swapState(this);
objSwap->playAnimation();
//We don't count it to traveling distance.
isTraveling=false;
//NOTE: Statistics updated in swapState() function.
}else{
//We can't swap so play the error sound.
getSoundManager()->playSound("error");
}
}
}else{
if(!(dead || objParent->shadow.dead)){
//Check if the player isn't in front of a shadow block.
if(!objShadowBlock.get()){
swapState(&objParent->shadow);
objSwap->playAnimation();
//We don't count it to traveling distance.
isTraveling=false;
//NOTE: Statistics updated in swapState() function.
}else{
//We can't swap so play the error sound.
getSoundManager()->playSound("error");
}
}
}
}
//Determine the correct theme state.
if(!dead){
//Set the direction depending on the velocity.
if(xVel>0)
direction=0;
else if(xVel<0)
direction=1;
//Check if the player is in the air.
if(!inAir){
//On the ground so check the direction and movement.
if(xVel>0){
appearance.changeState("walkright",true,true);
}else if(xVel<0){
appearance.changeState("walkleft",true,true);
}else if(xVel==0){
if(direction==1){
appearance.changeState("standleft",true,true);
}else{
appearance.changeState("standright",true,true);
}
}
}else{
//Check for jump appearance (inAir).
if(direction==1){
if(yVel>0){
appearance.changeState("fallleft",true,true);
}else{
appearance.changeState("jumpleft",true,true);
}
}else{
if(yVel>0){
appearance.changeState("fallright",true,true);
}else{
appearance.changeState("jumpright",true,true);
}
}
}
}
//Update traveling distance statistics.
if(isTraveling && (lastX!=box.x || lastY!=box.y) && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
float dx=float(lastX-box.x),dy=float(lastY-box.y);
float d0=statsMgr.playerTravelingDistance+statsMgr.shadowTravelingDistance,
d=sqrtf(dx*dx+dy*dy)/50.0f;
if(shadow) statsMgr.shadowTravelingDistance+=d;
else statsMgr.playerTravelingDistance+=d;
//Update achievement
d+=d0;
if(d0<=100.0f && d>=100.0f) statsMgr.newAchievement("travel100");
if(d0<=1000.0f && d>=1000.0f) statsMgr.newAchievement("travel1k");
if(d0<=10000.0f && d>=10000.0f) statsMgr.newAchievement("travel10k");
if(d0<=42195.0f && d>=42195.0f) statsMgr.newAchievement("travel42k");
}
//Reset the downKeyPressed flag.
downKeyPressed=false;
}
void Player::collision(vector<Block*> &levelObjects, Player* other){
//Only move when the player isn't dead.
if(dead)
return;
//First sort out the velocity.
//NOTE: This is the temporary xVel which takes canMove into consideration.
//This shadows Player::xVel.
const int xVel = canMove ? this->xVel : 0;
//Add gravity acceleration to the vertical velocity.
if(isJump)
jump();
if(inAir==true){
yVel+=1;
//Cap fall speed to 13.
if(yVel>13)
yVel=13;
}
Block* baseBlock=NULL;
if(auto tmp = objCurrentStand.get()) {
baseBlock=tmp;
} else if(other && other->holdingOther) {
//NOTE: this actually CAN happen, e.g. when player is holding shadow and the player is going to jump
//assert(other->objCurrentStand != NULL);
baseBlock=other->objCurrentStand.get();
}
if(baseBlock!=NULL){
//Now get the velocity and delta of the object the player is standing on.
SDL_Rect v=baseBlock->getBox(BoxType_Velocity);
SDL_Rect delta=baseBlock->getBox(BoxType_Delta);
switch(baseBlock->type){
//For conveyor belts the velocity is transfered.
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
xVelBase=v.x;
break;
//In other cases, such as player on shadow, player on crate. the change in x position must be considered.
default:
{
if(delta.x != 0)
xVelBase+=delta.x;
}
break;
}
//NOTE: Only copy the velocity of the block when moving down.
//Upwards is automatically resolved before the player is moved.
if(delta.y>0){
//Fixes the jitters when the player is on a pushable block on a downward moving box.
//NEW FIX: the squash bug. The following line of code is commented and change 'v' to 'delta'.
//box.y+=delta.y;
yVelBase=delta.y;
}
else
yVelBase=0;
}
//Set the object the player is currently standing to NULL.
objCurrentStand=NULL;
//Store the location of the player.
int lastX=box.x;
int lastY=box.y;
//An array that will hold all the GameObjects that are involved in the collision/movement.
vector<Block*> objects;
//All the blocks have moved so if there's collision with the player, the block moved into him.
for(unsigned int o=0;o<levelObjects.size();o++){
//Make sure to only check visible blocks.
if (levelObjects[o]->queryProperties(GameObjectProperty_Flags, this) & 0x80000000)
continue;
//Make sure the object is solid for the player.
if(!levelObjects[o]->queryProperties(GameObjectProperty_PlayerCanWalkOn,this))
continue;
//Check for collision.
if(checkCollision(box,levelObjects[o]->getBox()))
objects.push_back(levelObjects[o]);
}
//There was collision so try to resolve it.
if(!objects.empty()){
//FIXME: When multiple moving blocks are overlapping the player can be "bounced" off depending on the block order.
for(unsigned int o=0;o<objects.size();o++){
SDL_Rect r=objects[o]->getBox();
SDL_Rect delta=objects[o]->getBox(BoxType_Delta);
//Check on which side of the box the player is.
if(delta.x!=0){
if(delta.x>0){
//Move the player right if necessary.
if((r.x+r.w)-box.x<=delta.x && box.x<r.x+r.w)
box.x=r.x+r.w;
}else{
//Move the player left if necessary.
if((box.x+box.w)-r.x<=-delta.x && box.x>r.x-box.w)
box.x=r.x-box.w;
}
}
if(delta.y!=0){
if(delta.y>0){
//Move the player down if necessary.
if((r.y+r.h)-box.y<=delta.y && box.y<r.y+r.h)
box.y=r.y+r.h;
}else{
//Move the player up if necessary.
if((box.y+box.h)-r.y<=-delta.y && box.y>r.y-box.h)
box.y=r.y-box.h;
}
}
}
//Check if the player is squashed.
for(unsigned int o=0;o<levelObjects.size();o++){
//Make sure the object is visible.
if (levelObjects[o]->queryProperties(GameObjectProperty_Flags, this) & 0x80000000)
continue;
//Make sure the object is solid for the player.
if(!levelObjects[o]->queryProperties(GameObjectProperty_PlayerCanWalkOn,this))
continue;
if(checkCollision(box,levelObjects[o]->getBox())){
//The player is squashed so first move him back.
box.x=lastX;
box.y=lastY;
//Update statistics.
if(!dead && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
if(shadow) statsMgr.shadowSquashed++;
else statsMgr.playerSquashed++;
switch(statsMgr.playerSquashed+statsMgr.shadowSquashed){
case 1:
statsMgr.newAchievement("squash1");
break;
case 50:
statsMgr.newAchievement("squash50");
break;
}
}
//Now call the die method.
die();
return;
}
}
}
//Reuse the objects array, this time for blocks the player walks into.
objects.clear();
//Determine the collision frame.
SDL_Rect frame={box.x,box.y,box.w,box.h};
//Keep the horizontal movement of the player in mind.
if(xVel+xVelBase>=0){
frame.w+=(xVel+xVelBase);
}else{
frame.x+=(xVel+xVelBase);
frame.w-=(xVel+xVelBase);
}
//And the vertical movement.
if(yVel+yVelBase>=0){
frame.h+=(yVel+yVelBase);
}else{
frame.y+=(yVel+yVelBase);
frame.h-=(yVel+yVelBase);
}
//Loop through the game objects.
for(unsigned int o=0; o<levelObjects.size(); o++){
//Make sure the block is visible.
if (levelObjects[o]->queryProperties(GameObjectProperty_Flags, this) & 0x80000000)
continue;
//Check if the player can collide with this game object.
if(!levelObjects[o]->queryProperties(GameObjectProperty_PlayerCanWalkOn,this))
continue;
//Check if the block is inside the frame.
if(checkCollision(frame,levelObjects[o]->getBox()))
objects.push_back(levelObjects[o]);
}
//Horizontal pass.
if(xVel+xVelBase!=0){
box.x+=xVel+xVelBase;
for(unsigned int o=0;o<objects.size();o++){
SDL_Rect r=objects[o]->getBox();
if(!checkCollision(box,r))
continue;
//In case of a pushable block we give it velocity.
if(objects[o]->type==TYPE_PUSHABLE){
objects[o]->xVel+=(xVel+xVelBase)/2;
}
if(xVel+xVelBase>0){
//We came from the left so the right edge of the player must be less or equal than xVel+xVelBase.
if((box.x+box.w)-r.x<=xVel+xVelBase)
box.x=r.x-box.w;
}else{
//We came from the right so the left edge of the player must be greater or equal than xVel+xVelBase.
if(box.x-(r.x+r.w)>=xVel+xVelBase)
box.x=r.x+r.w;
}
}
}
//Some variables that are used in vertical movement.
Block* lastStand=NULL;
inAir=true;
//Vertical pass.
if(yVel+yVelBase!=0){
box.y+=yVel+yVelBase;
//Value containing the previous 'depth' of the collision.
int prevDepth=0;
for(unsigned int o=0;o<objects.size();o++){
SDL_Rect r=objects[o]->getBox();
if(!checkCollision(box,r))
continue;
//Now check how we entered the block (vertically or horizontally).
if(yVel+yVelBase>0){
//Calculate the number of pixels the player is in the block (vertically).
int depth=(box.y+box.h)-r.y;
//We came from the top so the bottom edge of the player must be less or equal than yVel+yVelBase.
if(depth<=yVel+yVelBase){
//NOTE: lastStand is handled later since the player can stand on only one block at the time.
//Check if there's already a lastStand.
if(lastStand){
//Since the player fell he will stand on the highest block, meaning the highest 'depth'.
if(depth>prevDepth){
lastStand=objects[o];
prevDepth=depth;
}else if(depth==prevDepth){
//Both blocks are at the same height so determine the block by the amount the player is standing on them.
SDL_Rect r=objects[o]->getBox();
int w=0;
if(box.x+box.w>r.x+r.w)
w=(r.x+r.w)-box.x;
else
w=(box.x+box.w)-r.x;
//Do the same for the other box.
r=lastStand->getBox();
int w2=0;
if(box.x+box.w>r.x+r.w)
w2=(r.x+r.w)-box.x;
else
w2=(box.x+box.w)-r.x;
//NOTE: It doesn't matter which block the player is on if they are both stationary.
SDL_Rect v=objects[o]->getBox(BoxType_Velocity);
SDL_Rect v2=lastStand->getBox(BoxType_Velocity);
//Either the have the same (vertical) velocity so most pixel standing on is the lastStand...
// ... OR one is moving slower down/faster up and that's the one the player is standing on.
if((v.y==v2.y && w>w2) || v.y<v2.y){
lastStand=objects[o];
prevDepth=depth;
}
}
}else{
//There isn't one so assume the current block for now.
lastStand=objects[o];
prevDepth=depth;
}
}
}else{
//We came from the bottom so the upper edge of the player must be greater or equal than yVel+yVelBase.
if(box.y-(r.y+r.h)>=yVel+yVelBase){
box.y=r.y+r.h;
yVel=0;
}
}
}
}
if(lastStand){
inAir=false;
yVel=1;
SDL_Rect r=lastStand->getBox();
box.y=r.y-box.h;
}
//Check if the player fell of the level, if so let him die but without animation.
if(box.y>objParent->levelRect.y+objParent->levelRect.h)
die(false);
//Check if the player changed blocks, meaning stepped onto a block.
objCurrentStand=lastStand;
auto ols = objLastStand.get();
if(lastStand!=ols){
//The player has changed block so call the playerleave event.
if(ols)
objParent->broadcastObjectEvent(GameObjectEvent_PlayerLeave,-1,NULL,ols);
//Set the new lastStand.
objLastStand=lastStand;
if(lastStand){
//NOTE: We partially revert this piece of code to that in commit 0072762,
//i.e. change the event GameObjectEvent_PlayerWalkOn from asynchronous back to synchronous,
//to fix the fragile block hit test bug when it is breaking.
//Hopefully it will not introduce bugs (e.g. bugs regarding dynamic add/delete of objects).
if (lastStand->type == TYPE_FRAGILE) {
//Call the walk on event of the laststand in a synchronous way.
lastStand->onEvent(GameObjectEvent_PlayerWalkOn);
//Bugfix for Fragile blocks.
if (!lastStand->queryProperties(GameObjectProperty_PlayerCanWalkOn, this)) {
inAir = true;
isJump = false;
}
} else {
//Call the walk on event of the laststand in an asynchronous way.
objParent->broadcastObjectEvent(GameObjectEvent_PlayerWalkOn, -1, NULL, lastStand);
}
}
}
//NOTE: The PlayerIsOn event must be handled here so that the script can change the location of a block without interfering with the collision detection.
//Handlingin it here also guarantees that this event will only be called once for one block per update.
if(lastStand)
objParent->broadcastObjectEvent(GameObjectEvent_PlayerIsOn,-1,NULL,lastStand);
//Reset the base velocity.
xVelBase=yVelBase=0;
canMove=true;
}
void Player::jump(int strength){
//Check if the player can jump.
if(inAir==false){
//Set the jump velocity.
yVel=-strength;
inAir=true;
isJump=false;
jumpTime++;
//Update statistics
if(!objParent->player.isPlayFromRecord() && !objParent->interlevel){
if(shadow) statsMgr.shadowJumps++;
else statsMgr.playerJumps++;
int tmp = statsMgr.playerJumps + statsMgr.shadowJumps;
switch (tmp) {
case 100:
statsMgr.newAchievement("jump100");
break;
case 1000:
statsMgr.newAchievement("jump1k");
break;
}
}
//Check if sound is enabled, if so play the jump sound.
getSoundManager()->playSound("jump");
}
}
void Player::updateAnimation() {
//Check if we should update the recorded line.
//Only do this when we're recording and we're not the shadow.
if (!shadow && record) {
//Add a new point.
line.push_back(SDL_Point{ box.x + 11, box.y + 20 });
}
//Also update the animation of appearance.
appearance.updateAnimation();
}
void Player::show(SDL_Renderer& renderer){
//Check if we should render the recorded line.
//Only do this when we're recording and we're not the shadow.
if (!shadow && record) {
//Loop through the line dots and draw them.
for (const SDL_Point& p : line) {
appearance.drawState("line", renderer, p.x - camera.x, p.y - camera.y);
}
}
appearance.draw(renderer, box.x-camera.x, box.y-camera.y);
}
void Player::shadowSetState(){
int currentKey=0;
/*//debug
extern int block_test_count;
extern bool block_test_only;
if(SDL_GetKeyState(NULL)[SDLK_p]){
block_test_count=recordButton.size();
}
if(block_test_count==(int)recordButton.size()){
block_test_only=true;
}*/
//Check if we should read the input from record file.
if(recordIndex>=0){ // && recordIndex<(int)recordButton.size()){
//read the input from record file
if(recordIndex<(int)recordButton.size()){
currentKey=recordButton[recordIndex];
recordIndex++;
}
//Reset horizontal velocity.
xVel=0;
if(currentKey&PlayerButtonRight){
//Walking to the right.
xVel+=7;
}
if(currentKey&PlayerButtonLeft){
//Walking to the left.
xVel-=7;
}
if(currentKey&PlayerButtonJump){
//The up key, if we aren't in the air we start jumping.
if(inAir==false){
isJump=true;
}else{
//Shouldn't go here
cout<<"Replay BUG"<<endl;
}
}
//check the down key
downKeyPressed=(currentKey&PlayerButtonDown)!=0;
//check the space key
if(currentKey&PlayerButtonSpace){
spaceKeyDown(&objParent->shadow);
}
}else{
//read the input from keyboard.
recordIndex=-1;
//Check for xvelocity.
if(xVel>0)
currentKey|=PlayerButtonRight;
if(xVel<0)
currentKey|=PlayerButtonLeft;
//Check for jumping.
if(isJump){
#ifdef RECORD_FILE_DEBUG
char c[64];
sprintf(c,"[%05d] Jump key recorded\n",objParent->time-1);
cout<<c;
recordKeyPressLog+=c;
#endif
currentKey|=PlayerButtonJump;
}
//Check if the downbutton is pressed.
if(downKeyPressed){
#ifdef RECORD_FILE_DEBUG
char c[64];
sprintf(c,"[%05d] Action key recorded\n",objParent->time-1);
cout<<c;
recordKeyPressLog+=c;
#endif
currentKey|=PlayerButtonDown;
}
if(spaceKeyPressed){
#ifdef RECORD_FILE_DEBUG
char c[64];
sprintf(c,"[%05d] Space key recorded\n",objParent->time-1);
cout<<c;
recordKeyPressLog+=c;
#endif
currentKey|=PlayerButtonSpace;
}
//Record it.
recordButton.push_back(currentKey);
}
#ifdef RECORD_FILE_DEBUG
if(recordIndex>=0){
if(recordIndex>0 && recordIndex<=int(recordPlayerPosition.size())/2){
SDL_Rect &r1=recordPlayerPosition[recordIndex*2-2];
SDL_Rect &r2=recordPlayerPosition[recordIndex*2-1];
if(r1.x!=box.x || r1.y!=box.y || r2.x!=objParent->shadow.box.x || r2.y!=objParent->shadow.box.y){
char c[192];
sprintf(c,"Replay ERROR [%05d] %d %d %d %d Expected: %d %d %d %d\n",
objParent->time-1,box.x,box.y,objParent->shadow.box.x,objParent->shadow.box.y,r1.x,r1.y,r2.x,r2.y);
cout<<c;
}
}
}else{
recordPlayerPosition.push_back(box);
recordPlayerPosition.push_back(objParent->shadow.box);
}
#endif
//reset spaceKeyPressed.
spaceKeyPressed=false;
//Only add an entry if the player is recording.
if(record){
//Add the action.
if(!dead && !objParent->shadow.dead){
playerButton.push_back(currentKey);
//Change the state.
state++;
}else{
//Either player or shadow is dead, stop recording.
playerButton.clear();
state=0;
record=false;
}
}
}
void Player::shadowGiveState(Shadow* shadow){
//Check if the player calls the shadow.
if(shadowCall==true){
//Clear any recording still with the shadow.
shadow->playerButton.clear();
//Loop the recorded moves and add them to the one of the shadow.
for(unsigned int s=0;s<playerButton.size();s++){
shadow->playerButton.push_back(playerButton[s]);
}
//Reset the state of both the player and the shadow.
stateReset();
shadow->stateReset();
//Clear the recording at the player's side.
playerButton.clear();
line.clear();
//Set shadowCall false
shadowCall=false;
//And let the shadow know that the player called him.
shadow->meCall();
}
}
void Player::stateReset(){
//Reset the state by setting it to 0.
state=0;
}
void Player::otherCheck(class Player* other){
//Now check if the player is on the shadow.
//First make sure they are both alive.
if(!dead && !other->dead){
//Get the box of the shadow.
SDL_Rect boxShadow=other->getBox();
//Check if the player is on top of the shadow.
if(checkCollision(box,boxShadow)==true){
//We have collision now check if the other is standing on top of you.
if(box.y+box.h<=boxShadow.y+13 && !other->inAir){
//Player is on shadow.
int yVelocity=yVel-1;
if(yVelocity>0){
//If the player is going to stand on the shadow for the first time, check if there are enough spaces for it.
if (!other->holdingOther) {
const SDL_Rect r = { box.x, boxShadow.y - box.h, box.w, box.h };
for (auto ooo : objParent->levelObjects){
//Make sure to only check visible blocks.
if (ooo->queryProperties(GameObjectProperty_Flags, this) & 0x80000000)
continue;
//Make sure the object is solid for the player.
if (!ooo->queryProperties(GameObjectProperty_PlayerCanWalkOn, this))
continue;
//Check for collision.
if (checkCollision(r, ooo->getBox())) {
//We are blocked hence we can't stand on it.
return;
}
}
}
box.y=boxShadow.y-box.h;
inAir=false;
canMove=false;
//Reset the vertical velocity.
yVel=2;
other->holdingOther=true;
other->appearance.changeState("holding");
//Change our own appearance to standing.
if(direction==1){
appearance.changeState("standleft");
}else{
appearance.changeState("standright");
}
//Set the velocity things.
objCurrentStand=NULL;
}
}else if(boxShadow.y+boxShadow.h<=box.y+13 && !inAir){
//Shadow is on player.
int yVelocity=other->yVel-1;
if(yVelocity>0){
//If the shadow is going to stand on the player for the first time, check if there are enough spaces for it.
if (!holdingOther) {
const SDL_Rect r = { boxShadow.x, box.y - boxShadow.h, boxShadow.w, boxShadow.h };
for (auto ooo : objParent->levelObjects){
//Make sure to only check visible blocks.
if (ooo->queryProperties(GameObjectProperty_Flags, other) & 0x80000000)
continue;
//Make sure the object is solid for the shadow.
if (!ooo->queryProperties(GameObjectProperty_PlayerCanWalkOn, other))
continue;
//Check for collision.
if (checkCollision(r, ooo->getBox())) {
//We are blocked hence we can't stand on it.
return;
}
}
}
other->box.y=box.y-boxShadow.h;
other->inAir=false;
other->canMove=false;
//Reset the vertical velocity of the other.
other->yVel=2;
holdingOther=true;
appearance.changeState("holding");
//Change our own appearance to standing.
if(other->direction==1){
other->appearance.changeState("standleft");
}else{
other->appearance.changeState("standright");
}
//Set the velocity things.
other->objCurrentStand=NULL;
}
}
}else{
holdingOther=false;
other->holdingOther=false;
}
}
}
SDL_Rect Player::getBox(){
return box;
}
void Player::setMyCamera(){
//Only change the camera when the player isn't dead.
if(dead)
return;
//Check if the level fit's horizontally inside the camera.
if(camera.w>objParent->levelRect.w){
camera.x=objParent->levelRect.x-(camera.w-objParent->levelRect.w)/2;
}else{
//Check if the player is halfway pass the halfright of the screen.
if(box.x>camera.x+(SCREEN_WIDTH/2+50)){
//It is so ease the camera to the right.
camera.x+=(box.x-camera.x-(SCREEN_WIDTH/2))>>4;
//Check if the camera isn't going too far.
if(box.x<camera.x+(SCREEN_WIDTH/2+50)){
camera.x=box.x-(SCREEN_WIDTH/2+50);
}
}
//Check if the player is halfway pass the halfleft of the screen.
if(box.x<camera.x+(SCREEN_WIDTH/2-50)){
//It is so ease the camera to the left.
camera.x+=(box.x-camera.x-(SCREEN_WIDTH/2))>>4;
//Check if the camera isn't going too far.
if(box.x>camera.x+(SCREEN_WIDTH/2-50)){
camera.x=box.x-(SCREEN_WIDTH/2-50);
}
}
//If the camera is too far to the left we set it to 0.
if(camera.x<objParent->levelRect.x){
camera.x=objParent->levelRect.x;
}
//If the camera is too far to the right we set it to the max right.
if(camera.x+camera.w>objParent->levelRect.x+objParent->levelRect.w){
camera.x=objParent->levelRect.x+objParent->levelRect.w-camera.w;
}
}
//Check if the level fit's vertically inside the camera.
if(camera.h>objParent->levelRect.h){
//We don't centre vertical because the bottom line of the level (deadly) will be mid air.
camera.y=objParent->levelRect.y-(camera.h-objParent->levelRect.h);
}else{
//Check if the player is halfway pass the lower half of the screen.
if(box.y>camera.y+(SCREEN_HEIGHT/2+50)){
//If is so ease the camera down.
camera.y+=(box.y-camera.y-(SCREEN_HEIGHT/2))>>4;
//Check if the camera isn't going too far.
if(box.y<camera.y+(SCREEN_HEIGHT/2+50)){
camera.y=box.y-(SCREEN_HEIGHT/2+50);
}
}
//Check if the player is halfway pass the upper half of the screen.
if(box.y<camera.y+(SCREEN_HEIGHT/2-50)){
//It is so ease the camera up.
camera.y+=(box.y-camera.y-(SCREEN_HEIGHT/2))>>4;
//Check if the camera isn't going too far.
if(box.y>camera.y+(SCREEN_HEIGHT/2-50)){
camera.y=box.y-(SCREEN_HEIGHT/2-50);
}
}
//If the camera is too far up we set it to 0.
if(camera.y<objParent->levelRect.y){
camera.y=objParent->levelRect.y;
}
//If the camera is too far down we set it to the max down.
if(camera.y+camera.h>objParent->levelRect.y+objParent->levelRect.h){
camera.y=objParent->levelRect.y+objParent->levelRect.h-camera.h;
}
}
}
void Player::reset(bool save){
//Set the location of the player to it's initial state.
box.x=fx;
box.y=fy;
//Reset back to default value.
inAir=true;
isJump=false;
shadowCall=false;
canMove=true;
holdingOther=false;
dead=false;
record=false;
downKeyPressed=false;
spaceKeyPressed=false;
//Some animation variables.
appearance = appearanceInitial;
if (save) appearanceSave = appearanceInitial;
direction=0;
state=0;
xVel=0; //??? fixed a strange bug in game replay
yVel=0;
//Reset the gameObject pointers.
objCurrentStand=objLastStand=objLastTeleport=objNotificationBlock=objShadowBlock=NULL;
if(save)
objCurrentStandSave=objLastStandSave=objLastTeleportSave=objNotificationBlockSave=objShadowBlockSave=NULL;
//Clear the recording.
line.clear();
playerButton.clear();
recordButton.clear();
recordIndex=-1;
#ifdef RECORD_FILE_DEBUG
recordKeyPressLog.clear();
recordPlayerPosition.clear();
#endif
if(save){
//xVelSaved is used to indicate if there's a state saved or not.
xVelSaved=0x80000000;
loadAndDieTimes=0;
}
}
+void Player::saveStateInternal(PlayerSaveState* o) {
+ o->boxSaved = box;
+ o->xVelSaved = xVel;
+ o->yVelSaved = yVel;
+ o->inAirSaved = inAir;
+ o->isJumpSaved = isJump;
+ o->canMoveSaved = canMove;
+ o->deadSaved = dead;
+ o->holdingOtherSaved = holdingOther;
+ o->stateSaved = state;
+
+ //Let the appearance save.
+ o->appearanceSave = appearance;
+
+ //Save the lastStand and currentStand pointers.
+ o->objCurrentStandSave = objCurrentStand;
+ o->objLastStandSave = objLastStand;
+ o->objLastTeleportSave = objLastTeleport;
+ o->objNotificationBlockSave = objNotificationBlock;
+ o->objShadowBlockSave = objShadowBlock;
+
+ //Save any recording stuff.
+ o->recordSaved = record;
+ o->playerButtonSaved = playerButton;
+ o->lineSaved = line;
+
+}
+
+void Player::loadStateInternal(PlayerSaveState* o) {
+ //Restore the saved values.
+ box = o->boxSaved;
+ //xVel is set to 0 since it's saved counterpart is used to indicate a saved state.
+ xVel = 0;
+ yVel = o->yVelSaved;
+
+ //Restore the saved values.
+ inAir = o->inAirSaved;
+ isJump = o->isJumpSaved;
+ canMove = o->canMoveSaved;
+ holdingOther = o->holdingOtherSaved;
+ dead = o->deadSaved;
+ record = false;
+ shadowCall = false;
+ state = o->stateSaved;
+
+ objCurrentStand = o->objCurrentStandSave;
+ objLastStand = o->objLastStandSave;
+ objLastTeleport = o->objLastTeleportSave;
+ objNotificationBlock = o->objNotificationBlockSave;
+ objShadowBlock = o->objShadowBlockSave;
+
+ //Restore the appearance.
+ appearance = o->appearanceSave;
+
+ //Restore any recorded stuff.
+ record = o->recordSaved;
+ playerButton = o->playerButtonSaved;
+ line = o->lineSaved;
+}
+
void Player::saveState(){
//We can only save the state when the player isn't dead.
if(!dead){
- boxSaved.x=box.x;
- boxSaved.y=box.y;
- xVelSaved=xVel;
- yVelSaved=yVel;
- inAirSaved=inAir;
- isJumpSaved=isJump;
- canMoveSaved=canMove;
- holdingOtherSaved=holdingOther;
- stateSaved=state;
-
- //Let the appearance save.
- appearanceSave = appearance;
-
- //Save the lastStand and currentStand pointers.
- objCurrentStandSave=objCurrentStand;
- objLastStandSave=objLastStand;
- objLastTeleportSave = objLastTeleport;
- objNotificationBlockSave = objNotificationBlock;
- objShadowBlockSave = objShadowBlock;
-
- //Save any recording stuff.
- recordSaved=record;
- playerButtonSaved=playerButton;
- lineSaved=line;
+ saveStateInternal(static_cast<PlayerSaveState*>(this));
//Save the record
savedRecordButton=recordButton;
#ifdef RECORD_FILE_DEBUG
recordKeyPressLog_saved=recordKeyPressLog;
recordPlayerPosition_saved=recordPlayerPosition;
#endif
//To prevent playing the sound twice, only the player can cause the sound.
if(!shadow)
getSoundManager()->playSound("checkpoint");
//We saved a new state so reset the counter
loadAndDieTimes=0;
}
}
void Player::loadState(){
//Check with xVelSaved if there's a saved state.
if(xVelSaved==int(0x80000000)){
//There isn't so reset the game to load the first initial state.
//NOTE: There's no need in removing the saved state since there is none.
reset(false);
return;
}
//Restore the saved values.
- box.x=boxSaved.x;
- box.y=boxSaved.y;
- //xVel is set to 0 since it's saved counterpart is used to indicate a saved state.
- xVel=0;
- yVel=yVelSaved;
-
- //Restore the saved values.
- inAir=inAirSaved;
- isJump=isJumpSaved;
- canMove=canMoveSaved;
- holdingOther=holdingOtherSaved;
- dead=false;
- record=false;
- shadowCall=false;
- state=stateSaved;
-
- objCurrentStand=objCurrentStandSave;
- objLastStand=objLastStandSave;
- objLastTeleport = objLastTeleportSave;
- objNotificationBlock = objNotificationBlockSave;
- objShadowBlock = objShadowBlockSave;
-
- //Restore the appearance.
- appearance = appearanceSave;
-
- //Restore any recorded stuff.
- record=recordSaved;
- playerButton=playerButtonSaved;
- line=lineSaved;
+ loadStateInternal(static_cast<PlayerSaveState*>(this));
//Load the previously saved record
recordButton=savedRecordButton;
#ifdef RECORD_FILE_DEBUG
recordKeyPressLog=recordKeyPressLog_saved;
recordPlayerPosition=recordPlayerPosition_saved;
#endif
}
void Player::swapState(Player* other){
//We need to swap the values of the player with the ones of the given player.
swap(box.x,other->box.x);
swap(box.y,other->box.y);
swap(xVelBase, other->yVelBase);
swap(yVelBase, other->yVelBase);
objCurrentStand.swap(other->objCurrentStand);
//NOTE: xVel isn't there since it's used for something else.
swap(yVel,other->yVel);
swap(inAir,other->inAir);
swap(isJump,other->isJump);
swap(canMove,other->canMove);
swap(holdingOther,other->holdingOther);
swap(dead, other->dead);
//Also reset the state of the other.
other->stateReset();
//Play the swap sound.
getSoundManager()->playSound("swap");
//Update statistics.
if(!dead && !objParent->player.isPlayFromRecord() && !objParent->interlevel){
if(objParent->time < objParent->recentSwap + FPS){
//Swap player and shadow twice in 1 senond.
statsMgr.newAchievement("quickswap");
}
objParent->recentSwap=objParent->time;
statsMgr.swapTimes++;
//Update achievements
switch(statsMgr.swapTimes){
case 100:
statsMgr.newAchievement("swap100");
break;
}
}
}
bool Player::canSaveState(){
//We can only save the state if the player isn't dead.
return !dead;
}
bool Player::canLoadState(){
//We use xVelSaved to indicate if a state is saved or not.
return xVelSaved != int(0x80000000);
}
void Player::die(bool animation){
//Make sure the player isn't already dead.
if(!dead){
dead=true;
//In the arcade mode, the game finishes when the player (not the shadow) dies
if (objParent->arcade && !shadow) {
objParent->won = true;
}
//If sound is enabled run the hit sound.
getSoundManager()->playSound("hit");
//Change the apearance to die (if animation is true).
if(animation){
if(direction==1){
appearance.changeState("dieleft");
}else{
appearance.changeState("dieright");
}
} else {
appearance.changeState("dead");
}
//Update statistics
if(!objParent->player.isPlayFromRecord() && !objParent->interlevel){
addRecentDeaths(objParent->recentLoad);
if(shadow) statsMgr.shadowDies++;
else statsMgr.playerDies++;
switch(statsMgr.playerDies+statsMgr.shadowDies){
case 1:
statsMgr.newAchievement("die1");
break;
case 50:
statsMgr.newAchievement("die50");
break;
case 1000:
statsMgr.newAchievement("die1000");
break;
}
if(canLoadState() && (++loadAndDieTimes)==100){
statsMgr.newAchievement("loadAndDie100");
}
if(objParent->player.dead && objParent->shadow.dead) statsMgr.newAchievement("doubleKill");
}
}
//We set the jumpTime to 80 when this is the shadow.
//That's the countdown for the "Your shadow has died." message.
if(shadow){
jumpTime=80;
}
}
diff --git a/src/Player.h b/src/Player.h
index 36eb01f..54667a0 100644
--- a/src/Player.h
+++ b/src/Player.h
@@ -1,270 +1,291 @@
/*
* Copyright (C) 2011-2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PLAYER_H
#define PLAYER_H
#include "ThemeManager.h"
#include "Block.h"
#include <vector>
#include <string>
#include <SDL.h>
//Debug the game record file.
//#define RECORD_FILE_DEBUG
class Game;
class PlayerScriptAPI;
//The different player buttons.
//The right arrow.
const int PlayerButtonRight=0x01;
//The left arrow.
const int PlayerButtonLeft=0x02;
//The up arrow for jumping.
const int PlayerButtonJump=0x04;
//The down arrow for actions.
const int PlayerButtonDown=0x08;
//space bar for recording. (Only in recordButton)
const int PlayerButtonSpace=0x10;
-class Player{
- friend class PlayerScriptAPI;
-protected:
- //Vector used to store the player actions in when recording.
- //These can be given to the shadow so he can execute them.
- std::vector<int> playerButton;
+struct PlayerSaveState {
//Vector used to store the playerButton vector when saving the player's state (checkpoint).
std::vector<int> playerButtonSaved;
-private:
- //Vector used to record the whole game play.
- //And saved record in checkpoint.
- std::vector<int> recordButton,savedRecordButton;
-
- //record index. -1 means read input from keyboard,
- //otherwise read input from recordings (recordButton[recordIndex]).
- int recordIndex;
-
- //Vector containing squares along the path the player takes when recording.
- //It will be drawn as a trail of squares.
- std::vector<SDL_Point> line;
//Vector that will hold the line vector when saving the player's state (checkpoint).
std::vector<SDL_Point> lineSaved;
- //Boolean if the player called the shadow to copy his moves.
- bool shadowCall;
//Boolean if the player is recording his moves.
- bool record,recordSaved;
-
+ bool recordSaved;
+
//The following variables are to store a state.
//Rectangle containing the players location.
SDL_Rect boxSaved;
//Boolean if the player is in the air.
bool inAirSaved;
//Boolean if the player is (going to) jump(ing).
bool isJumpSaved;
//Boolean if the player can move.
bool canMoveSaved;
+ //Boolean if the player is dead.
+ bool deadSaved;
//Boolean if the player is holding the other (shadow).
bool holdingOtherSaved;
//The x velocity.
//NOTE: The x velocity is used to indicate that there's a state saved.
int xVelSaved;
//The y velocity.
int yVelSaved;
//The state.
int stateSaved;
-
+
+ //The save variable for the GameObject pointers.
+ //FIXME: Also save the other game object pointers?
+ Block::ObservePointer objCurrentStandSave;
+ Block::ObservePointer objLastStandSave;
+ Block::ObservePointer objLastTeleportSave;
+ Block::ObservePointer objNotificationBlockSave;
+ Block::ObservePointer objShadowBlockSave;
+
+ //The appearance of the player.
+ ThemeBlockInstance appearanceSave;
+
+ //Boolean if the shadow is called by the player.
+ //If so the shadow will copy the moves the player made.
+ bool calledSaved;
+};
+
+class Player : protected PlayerSaveState {
+ friend class PlayerScriptAPI;
+protected:
+ //Vector used to store the player actions in when recording.
+ //These can be given to the shadow so he can execute them.
+ std::vector<int> playerButton;
+
+private:
+ //Vector used to record the whole game play.
+ //And saved record in checkpoint.
+ std::vector<int> recordButton, savedRecordButton;
+
+ //record index. -1 means read input from keyboard,
+ //otherwise read input from recordings (recordButton[recordIndex]).
+ int recordIndex;
+
+ //Vector containing squares along the path the player takes when recording.
+ //It will be drawn as a trail of squares.
+ std::vector<SDL_Point> line;
+
+ //Boolean if the player called the shadow to copy his moves.
+ bool shadowCall;
+ //Boolean if the player is recording his moves.
+ bool record;
+
protected:
//Rectangle containing the player's location.
SDL_Rect box;
//The x and y velocity.
int xVel, yVel;
//The base x and y velocity, used for standing on moving blocks.
int xVelBase, yVelBase;
//Boolean if the player is in the air.
bool inAir;
//Boolean if the player is (going to) jump(ing).
bool isJump;
//Boolean if the player can move.
bool canMove;
//Boolean if the player is dead.
bool dead;
//The direction the player is walking, 0=right, 1=left.
int direction;
//Integer containing the state of the player.
int state;
//The time the player is in the air (jumping).
int jumpTime;
//Boolean if the player is in fact the shadow.
bool shadow;
//Pointer to the Game state.
friend class Game;
Game* objParent;
//Boolean if the downkey is pressed.
bool downKeyPressed;
//Boolean if the space keu is pressed.
bool spaceKeyPressed;
//Pointer to the object that is currently been stand on by the player.
//This is always a valid pointer.
Block::ObservePointer objCurrentStand;
//Pointer to the object the player stood last on.
//NOTE: This is a weak reference only.
Block::ObservePointer objLastStand;
//Pointer to the teleporter the player last took.
//NOTE: This is a weak reference only.
Block::ObservePointer objLastTeleport;
//Pointer to the notification block the player is in front of.
//This is always a valid pointer.
Block::ObservePointer objNotificationBlock;
//Pointer to the shadow block the player is in front of.
//This is always a valid pointer.
Block::ObservePointer objShadowBlock;
- //The save variable for the GameObject pointers.
- //FIXME: Also save the other game object pointers?
- Block::ObservePointer objCurrentStandSave;
- Block::ObservePointer objLastStandSave;
- Block::ObservePointer objLastTeleportSave;
- Block::ObservePointer objNotificationBlockSave;
- Block::ObservePointer objShadowBlockSave;
-
public:
//X and y location where the player starts and gets when reseted.
int fx, fy;
//The appearance of the player.
- ThemeBlockInstance appearance, appearanceSave, appearanceInitial;
+ ThemeBlockInstance appearance, appearanceInitial;
//Boolean if the player is holding the other.
bool holdingOther;
//Constructor.
//objParent: Pointer to the Game state.
Player(Game* objParent);
//Destructor.
~Player();
//Method used to set the position of the player.
//x: The new x location of the player.
//y: The new y location of the player.
void setLocation(int x,int y);
//Method used to handle (key) input.
//shadow: Pointer to the shadow used for recording/calling.
void handleInput(class Shadow* shadow);
//Method used to do the movement of the player.
//NOTE: should call collision() for both player/shadow before call move().
//levelObjects: Array containing the levelObjects, used to check collision.
//lastX, lastY: the position of player before calling collision().
void move(std::vector<Block*> &levelObjects, int lastX, int lastY);
//Method used to check if the player can jump and executes the jump.
//strength: The strength of the jump.
void jump(int strength=13);
//This method will update the animation.
void updateAnimation();
//This method will render the player to the screen.
void show(SDL_Renderer &renderer);
//Method that stores the actions if the player is recording.
void shadowSetState();
//Method that will reset the state to 0.
virtual void stateReset();
//This method checks the player against the other to see if they stand on eachother.
//other: The shadow or the player.
void otherCheck(class Player* other);
//Method that will ease the camera so that the player is in the center.
void setMyCamera();
//This method will reset the player to it's initial position.
//save: Boolean if the saved state should also be deleted.
void reset(bool save);
//Method used to retrieve the current location of the player.
//Returns: SDL_Rect containing the player's location.
SDL_Rect getBox();
//This method will
void shadowGiveState(class Shadow* shadow);
- //Method that will save the current state.
+ //Method that will save the current state. Only works if player and shadow are both alive.
//NOTE: The special <name>Saved variables will be used.
virtual void saveState();
//Method that will retrieve the last saved state.
//If there is none it will reset the player.
virtual void loadState();
//Method that checks if the player can save the state.
//Returns: True if the player can save his state.
virtual bool canSaveState();
//Method that checks if the player can load a state.
//Returns: True if the player can load a state.
virtual bool canLoadState();
//Method that will swap the state of the player with the other.
//other: The player or the shadow.
void swapState(Player* other);
+ //Internal function to save current state. Will not update achievement and statistics.
+ virtual void saveStateInternal(PlayerSaveState* o);
+ //Internal function to load the state. Will not update achievement and statistics.
+ virtual void loadStateInternal(PlayerSaveState* o);
+
//Check if this player is in fact the shadow.
//Returns: True if this is the shadow.
inline bool isShadow(){
return shadow;
}
//Method for returning the objCurrentStand pointer.
//Returns: Pointer to the gameobject the player is standing on.
inline Block* getObjCurrentStand(){
return objCurrentStand.get();
}
//Let the player die when he falls of or hits spikes.
//animation: Boolean if the death animation should be played, default is true.
void die(bool animation=true);
//Check if currently it's play from record file.
bool isPlayFromRecord();
//get the game record object.
std::vector<int>* getRecord();
#ifdef RECORD_FILE_DEBUG
std::string& keyPressLog();
std::vector<SDL_Rect>& playerPosition();
#endif
//play the record.
void playRecord();
private:
//The space key is down. call this function from handleInput and another function.
void spaceKeyDown(class Shadow* shadow);
public:
//Method that will handle the actual movement.
//NOTE: partially internal function. Should call collision() for both player/shadow before call move().
//levelObjects: Array containing the levelObjects, used to check collision.
void collision(std::vector<Block*> &levelObjects, Player* other);
};
#endif
diff --git a/src/Shadow.cpp b/src/Shadow.cpp
index 78a4bcb..9d9ddd6 100644
--- a/src/Shadow.cpp
+++ b/src/Shadow.cpp
@@ -1,88 +1,88 @@
/*
* Copyright (C) 2011-2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Functions.h"
#include "FileManager.h"
#include "Game.h"
#include "Player.h"
#include "Shadow.h"
#include <vector>
#include <iostream>
using namespace std;
Shadow::Shadow(Game* objParent):Player(objParent){
//Most of the initialising happens in the Player's constructor.
//Here we only set some shadow specific options.
called=calledSaved=false;
shadow=true;
}
void Shadow::moveLogic(){
//If we're called and there are still moves left we to that move.
if(called && !dead && state < (signed)playerButton.size()){
int currentKey=playerButton[state];
xVel=0;
//Check if the current move is walking.
if(currentKey & PlayerButtonRight) xVel=7;
if(currentKey & PlayerButtonLeft) xVel=-7;
//Check if the current move is jumping.
if((currentKey & PlayerButtonJump) && !inAir){
isJump=true;
}else{
isJump=false;
}
//Check if the current move is an action (DOWN arrow key).
if(currentKey & PlayerButtonDown){
downKeyPressed=true;
}else{
downKeyPressed=false;
}
//We've done the move so move on to the next one.
state++;
}else{
//We ran out of moves so reset it.
//FIXME: Every frame when called is false this will be done?
called=false;
state=0;
xVel=0;
}
}
void Shadow::meCall(){
called=true;
}
void Shadow::stateReset(){
state=0;
called=false;
}
-void Shadow::saveState(){
- Player::saveState();
- calledSaved=called;
+void Shadow::saveStateInternal(PlayerSaveState* o) {
+ Player::saveStateInternal(o);
+ o->calledSaved = called;
}
-void Shadow::loadState(){
- Player::loadState();
- called=calledSaved;
+void Shadow::loadStateInternal(PlayerSaveState* o) {
+ Player::loadStateInternal(o);
+ called = o->calledSaved;
}
diff --git a/src/Shadow.h b/src/Shadow.h
index 7bfd9f9..5901865 100644
--- a/src/Shadow.h
+++ b/src/Shadow.h
@@ -1,53 +1,53 @@
/*
* Copyright (C) 2011-2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SHADOW_H
#define SHADOW_H
#include "Player.h"
//The shadow class, it extends the player class since their almost the same.
class Shadow : public Player{
protected:
//Boolean if the shadow is called by the player.
//If so the shadow will copy the moves the player made.
- bool called,calledSaved;
+ bool called;
friend class Player;
public:
//Constructor, it sets a few variables and calls the Player's constructor.
//objParent: Pointer to the game instance.
Shadow(Game* objParent);
//Method that's called before the move function.
//It's used to let the shadow do his logic, moving and jumping.
void moveLogic();
//Method used to notify the shadow that he is called.
//He then must copy the moves that are given to him.
void meCall();
//Method used to reset the state.
virtual void stateReset() override;
- //Method used to save the state.
- virtual void saveState() override;
- //Method used to load the state.
- virtual void loadState() override;
+ //Internal function to save current state. Will not update achievement and statistics.
+ virtual void saveStateInternal(PlayerSaveState* o) override;
+ //Internal function to load the state. Will not update achievement and statistics.
+ virtual void loadStateInternal(PlayerSaveState* o) override;
};
#endif

File Metadata

Mime Type
text/x-diff
Expires
Fri, May 8, 8:23 PM (1 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
59797
Default Alt Text
(173 KB)

Event Timeline