Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
108 KB
Referenced Files
None
Subscribers
None
diff --git a/data/gfx/achievements/collect1k.png b/data/gfx/achievements/collect1k.png
new file mode 100644
index 0000000..14a3fc8
Binary files /dev/null and b/data/gfx/achievements/collect1k.png differ
diff --git a/src/AchievementList.cpp b/src/AchievementList.cpp
index 1f88d25..6196fab 100644
--- a/src/AchievementList.cpp
+++ b/src/AchievementList.cpp
@@ -1,115 +1,118 @@
/*
* Copyright (C) 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 "AchievementList.h"
// We redefined it here to prevent including more header files.
#ifndef __
#define __(X) (X)
#endif
// Format: {<id>, <name>, <file>, <pos>, <description>, <type>, nullptr}
// id: Identifier of achievement. Should not change once the new achievement is added,
// otherwise the old statistics will be lost.
// name: Name of achievement. Translatable.
// file: The icon file. NULL for no icon.
// pos: Specifies which part of the icon will be displayed (left, top, width, height).
// description: Description of achievement. Translatable. Can be multi-line text.
// type [optional]: Specifies the display type of achievement.
// ACHIEVEMENT_HIDDEN [default]: Show "Unknown achievement" when unfinished.
// ACHIEVEMENT_TITLE: Only show icon and title when unfinished.
// ACHIEVEMENT_ALL: Always show icon, title and description.
// ACHIEVEMENT_PROGRESS: Show icon, title and description and a progress bar.
// StatisticsManager::getAchievementProgress() function should return the progress (between 0 and 1).
// NOTE: WARNING: All arguments should now be specified, as surface being nullptr is being relied upon.
AchievementInfo achievementList[]={
{"newbie",__("Newbie"),"gfx/medals.png",{0,0,30,30},__("Complete a level."),ACHIEVEMENT_ALL,nullptr},
{"tutorial",__("Graduate"),"gfx/achievements/tutorial.png",{0,0,51,27},__("Complete the tutorial level pack."),ACHIEVEMENT_PROGRESS,nullptr},
{"experienced",__("Experienced player"),"gfx/achievements/experienced.png",{0,0,51,51},__("Complete 50 levels."),ACHIEVEMENT_PROGRESS,nullptr},
{"goodjob",__("Good job!"),"gfx/medals.png",{60,0,30,30},__("Receive a gold medal."),ACHIEVEMENT_ALL,nullptr},
{"tutorialGold",__("Outstanding graduate"),"gfx/achievements/tutorialgold.png",{0,0,51,27},__("Complete the tutorial level pack with gold for all levels."),ACHIEVEMENT_PROGRESS,nullptr},
{"expert",__("Expert"),"gfx/achievements/expert.png",{0,0,51,51},__("Earn 50 gold medals."),ACHIEVEMENT_PROGRESS,nullptr},
{"addicted",__("Hooked"),"gfx/achievements/addicted.png",{0,0,50,50},__("Play Me and My Shadow for more than 2 hours."),ACHIEVEMENT_TITLE,nullptr},
{"loyalFan",__("Loyal fan of Me and My Shadow"),"gfx/achievements/loyalfan.png",{0,0,50,50},__("Play Me and My Shadow for more than 24 hours."),ACHIEVEMENT_HIDDEN,nullptr},
{"constructor",__("Constructor"),"gfx/achievements/constructor.png",{0,0,50,50},__("Use the level editor for more than 2 hours."),ACHIEVEMENT_HIDDEN,nullptr},
{"constructor2",__("The creator"),"gfx/achievements/constructor2.png",{0,0,50,50},__("Use the level editor for more than 8 hours."),ACHIEVEMENT_HIDDEN,nullptr},
{"create1",__("Look, cute level!"),"gfx/addon1.png",{0,0,64,64},__("Create a level for the first time."),ACHIEVEMENT_ALL,nullptr},
{"create10",__("The level museum"),"gfx/addon2.png",{0,0,64,64},__("Create 10 levels."),ACHIEVEMENT_PROGRESS,nullptr},
{ "helloworld", __("Hello, World!"), "gfx/achievements/helloworld.png", { 0, 0, 50, 50 }, __("Write a script for the first time."), ACHIEVEMENT_TITLE, nullptr },
{ "jump100", __("Frog"), "themes/Cloudscape/characters/player.png", { 230, 0, 23, 40 }, __("Jump 100 times."), ACHIEVEMENT_PROGRESS, nullptr },
{ "jump1k", __("Kangaroo"), "themes/Cloudscape/characters/player.png", { 230, 0, 23, 40 }, __("Jump 1000 times."), ACHIEVEMENT_PROGRESS, nullptr },
{"travel100",__("Wanderer"),"themes/Cloudscape/characters/player.png",{69,0,23,40},__("Travel 100 meters."),ACHIEVEMENT_PROGRESS,nullptr},
{"travel1k",__("Runner"),"themes/Cloudscape/characters/player.png",{23,0,23,40},__("Travel 1 kilometer."),ACHIEVEMENT_PROGRESS,nullptr},
{"travel10k",__("Long distance runner"),"themes/Cloudscape/characters/player.png",{46,0,23,40},__("Travel 10 kilometers."),ACHIEVEMENT_PROGRESS,nullptr},
{"travel42k",__("Marathon runner"),"themes/Cloudscape/characters/player.png",{92,0,23,40},__("Travel 42,195 meters."),ACHIEVEMENT_PROGRESS,nullptr},
{"die1",__("Be careful!"),"themes/Cloudscape/characters/deathright.png",{0,14,23,40},__("Die for the first time."),ACHIEVEMENT_ALL,nullptr},
{"die50",__("It doesn't matter..."),"gfx/achievements/die50.png",{0,0,50,50},__("Die 50 times."),ACHIEVEMENT_HIDDEN,nullptr},
{"die1000",__("Expert of trial and error"),"gfx/achievements/die1000.png",{0,0,50,50},__("Die 1000 times."),ACHIEVEMENT_HIDDEN,nullptr},
{"squash1",__("Keep an eye for moving blocks!"),"gfx/achievements/squash1.png",{0,0,50,50},__("Get squashed for the first time."),ACHIEVEMENT_HIDDEN,nullptr},
{"squash50",__("Potato masher"),"gfx/achievements/squash50.png",{0,0,50,50},__("Get squashed 50 times."),ACHIEVEMENT_HIDDEN,nullptr},
{"doubleKill",__("Double kill"),"gfx/achievements/doublekill.png",{0,0,50,50},__("Get both the player and the shadow dead."),ACHIEVEMENT_HIDDEN,nullptr},
{"die5in5",__("Bad luck"),"gfx/achievements/die5in5.png",{0,0,50,50},__("Die 5 times in under 5 seconds."),ACHIEVEMENT_TITLE,nullptr},
{"die10in5",__("This level is too dangerous"),"gfx/achievements/die10in5.png",{0,0,50,50},__("Die 10 times in under 5 seconds."),ACHIEVEMENT_HIDDEN,nullptr},
{"forget",__("You forgot your friend"),"gfx/achievements/forget.png",{0,0,45,53},__("Finish the level with the player or the shadow dead."),ACHIEVEMENT_HIDDEN,nullptr},
{"jit",__("Just in time"),"gfx/achievements/jit.png",{0,0,50,50},__("Reach the exit with the player and the shadow simultaneously."),ACHIEVEMENT_TITLE,nullptr},
{"record100",__("Recorder"),"gfx/achievements/record100.png",{0,0,50,50},__("Record 100 times."),ACHIEVEMENT_PROGRESS,nullptr},
{"record1k",__("Shadowmaster"),"themes/Cloudscape/characters/shadow.png",{23,0,23,40},__("Record 1000 times."),ACHIEVEMENT_PROGRESS,nullptr},
{"switch100",__("Switch puller"),"themes/Cloudscape/tiles/tiles.png",{100,100,50,50},__("Pull the switch 100 times."),ACHIEVEMENT_PROGRESS,nullptr},
{"switch1k",__("The switch is broken!"),"gfx/achievements/switch1k.png",{0,0,50,50},__("Pull the switch 1000 times."),ACHIEVEMENT_HIDDEN,nullptr},
{"swap100",__("Swapper"),"themes/Cloudscape/tiles/swap.png",{0,0,50,50},__("Swap 100 times."),ACHIEVEMENT_PROGRESS,nullptr},
{"save100",__("Play it save"),"gfx/achievements/save1k.png",{0,0,50,50},__("Save 100 times."),ACHIEVEMENT_HIDDEN,nullptr},
{"load100",__("This game is too hard"),"gfx/achievements/load1k.png",{0,0,50,50},__("Load the game 100 times."),ACHIEVEMENT_HIDDEN,nullptr},
+ { "collect100", __("Enriched"), "themes/Cloudscape/tiles/tiles.png", { 50, 150, 50, 50 }, __("Collect 100 collectibles."), ACHIEVEMENT_PROGRESS, nullptr },
+ { "collect1k", __("Treasure hunter"), "gfx/achievements/collect1k.png", { 0, 0, 50, 50 }, __("Collect 1000 collectibles."), ACHIEVEMENT_HIDDEN, nullptr },
+
{ "withoutsave", __("No, thanks"), "gfx/achievements/withoutsave.png", { 0, 0, 50, 50 }, __("Complete a level with checkpoint, but without saving."), ACHIEVEMENT_TITLE, nullptr },
{"panicSave",__("Panic save"),"gfx/achievements/panicsave.png",{0,0,50,50},__("Save twice in 1 second."),ACHIEVEMENT_HIDDEN,nullptr},
{"panicLoad",__("Panic load"),"gfx/achievements/panicload.png",{0,0,50,50},__("Load twice in 1 second."),ACHIEVEMENT_HIDDEN,nullptr},
{"loadAndDie",__("Bad saving position"),"gfx/achievements/loadanddie.png",{0,0,50,50},__("Load the game and die within 1 second."),ACHIEVEMENT_TITLE,nullptr},
{"loadAndDie100",__("This level is too hard"),"gfx/achievements/loadanddie100.png",{0,0,50,50},__("Load the same save and die 100 times."),ACHIEVEMENT_HIDDEN,nullptr},
{"quickswap",__("Quick swap"),"gfx/achievements/quickswap.png",{0,0,50,50},__("Swap twice in under a second."),ACHIEVEMENT_HIDDEN,nullptr},
//ripped from Achievements Unlocked
{"horizontal",__("Horizontal confusion"),"gfx/emotions.png",{0,0,23,40},__("Press left and right simultaneously."),ACHIEVEMENT_HIDDEN,nullptr},
{ "cheat", __("Cheater"), "gfx/achievements/cheat.png", { 0, 0, 50, 50 }, __("Cheat in game."), ACHIEVEMENT_HIDDEN, nullptr },
{"programmer",__("Programmer"),"gfx/achievements/programmer.png",{0,0,50,50},__("Play the development version of Me and My Shadow."),ACHIEVEMENT_TITLE,nullptr},
//end of achievements
{}
};
diff --git a/src/Player.cpp b/src/Player.cpp
index 17fcb25..9976a02 100644
--- a/src/Player.cpp
+++ b/src/Player.cpp
@@ -1,1755 +1,1762 @@
/*
* 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){
//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 = inAirSaved = true;
isJump = isJumpSaved = false;
shadowCall=false;
shadow=false;
canMove = canMoveSaved = true;
holdingOther = holdingOtherSaved = false;
dead=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(int recIndex){
recordIndex = recIndex;
}
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]);
}
}
}
+
+ //Update statistics.
+ if (!objParent->player.isPlayFromRecord() && !objParent->interlevel){
+ statsMgr.collectibleCollected++;
+ if (statsMgr.collectibleCollected == 100) statsMgr.newAchievement("collect100");
+ if (statsMgr.collectibleCollected == 1000) statsMgr.newAchievement("collect1k");
+ }
}
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) {
if (o == NULL) o = static_cast<PlayerSaveState*>(this);
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) {
if (o == NULL) o = static_cast<PlayerSaveState*>(this);
//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){
saveStateInternal();
//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.
loadStateInternal();
//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/StatisticsManager.cpp b/src/StatisticsManager.cpp
index 83114d0..317b6df 100644
--- a/src/StatisticsManager.cpp
+++ b/src/StatisticsManager.cpp
@@ -1,822 +1,827 @@
/*
* Copyright (C) 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 "StatisticsManager.h"
#include "FileManager.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "Functions.h"
#include "LevelPackManager.h"
#include "MusicManager.h"
#include "SoundManager.h"
#include "ThemeManager.h"
#include "WordWrapper.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#include "libs/tinyformat/tinyformat.h"
#include <SDL_ttf_fontfallback.h>
#if defined(WIN32)
#define PRINTF_LONGLONG "%I64d"
#else
#define PRINTF_LONGLONG "%lld"
#endif
using namespace std;
StatisticsManager statsMgr;
static const int achievementDisplayTime=(FPS*4500)/1000;
static const int achievementIntervalTime=achievementDisplayTime+(FPS*500)/1000;
static map<string,AchievementInfo*> avaliableAchievements;
//================================================================
StatisticsManager::StatisticsManager(){
bmDropShadow=NULL;
bmQuestionMark=NULL;
bmAchievement=NULL;
startTime=time(NULL);
tutorialLevels=0;
clear();
}
void StatisticsManager::clear(){
playerTravelingDistance=shadowTravelingDistance=0.0f;
playerJumps=shadowJumps
=playerDies=shadowDies
=playerSquashed=shadowSquashed
=completedLevels=silverLevels=goldLevels
=recordTimes=switchTimes=swapTimes=saveTimes=loadTimes
+ =collectibleCollected
=playTime=levelEditTime
=createdLevels=tutorialCompleted=tutorialGold=0;
achievements.clear();
queuedAchievements.clear();
achievementTime=0;
currentAchievement=0;
if(bmAchievement){
bmAchievement.reset();
}
}
#define LOAD_STATS(var,func) { \
vector<string> &v=node.attributes[ #var ]; \
if(!v.empty() && !v[0].empty()) \
var=func(v[0].c_str()); \
}
void StatisticsManager::loadFile(const std::string& fileName){
clear();
ifstream file(fileName.c_str());
if(!file) return;
TreeStorageNode node;
POASerializer serializer;
if(!serializer.readNode(file,&node,true)) return;
//load statistics
LOAD_STATS(playerTravelingDistance,atof);
LOAD_STATS(shadowTravelingDistance,atof);
LOAD_STATS(playerJumps,atoi);
LOAD_STATS(shadowJumps,atoi);
LOAD_STATS(playerDies,atoi);
LOAD_STATS(shadowDies,atoi);
LOAD_STATS(playerSquashed,atoi);
LOAD_STATS(shadowSquashed,atoi);
LOAD_STATS(recordTimes,atoi);
LOAD_STATS(switchTimes,atoi);
LOAD_STATS(swapTimes,atoi);
LOAD_STATS(saveTimes,atoi);
LOAD_STATS(loadTimes,atoi);
+ LOAD_STATS(collectibleCollected,atoi);
LOAD_STATS(playTime,atoi);
LOAD_STATS(levelEditTime,atoi);
LOAD_STATS(createdLevels,atoi);
//load achievements.
//format is: name;time,name;time,...
{
vector<string> &v=node.attributes["achievements"];
for(unsigned int i=0;i<v.size();i++){
string s=v[i];
time_t t=0;
string::size_type lps=s.find(';');
if(lps!=string::npos){
string s1=s.substr(lps+1);
s=s.substr(0,lps);
long long n;
sscanf(s1.c_str(),PRINTF_LONGLONG,&n);
t=(time_t)n;
}
map<string,AchievementInfo*>::iterator it=avaliableAchievements.find(s);
if(it!=avaliableAchievements.end()){
OwnedAchievement ach={t,it->second};
achievements[it->first]=ach;
}
}
}
}
//Call when level edit is start
void StatisticsManager::startLevelEdit(){
levelEditStartTime=time(NULL);
}
//Call when level edit is end
void StatisticsManager::endLevelEdit(){
levelEditTime+=time(NULL)-levelEditStartTime;
}
//update in-game time
void StatisticsManager::updatePlayTime(){
time_t endTime=time(NULL);
playTime+=endTime-startTime;
startTime=endTime;
}
#define SAVE_STATS(var,pattern) { \
sprintf(s,pattern,var); \
node.attributes[ #var ].push_back(s); \
}
void StatisticsManager::saveFile(const std::string& fileName){
char s[64];
//update in-game time
updatePlayTime();
ofstream file(fileName.c_str());
if(!file) return;
TreeStorageNode node;
//save statistics
SAVE_STATS(playerTravelingDistance,"%.2f");
SAVE_STATS(shadowTravelingDistance,"%.2f");
SAVE_STATS(playerJumps,"%d");
SAVE_STATS(shadowJumps,"%d");
SAVE_STATS(playerDies,"%d");
SAVE_STATS(shadowDies,"%d");
SAVE_STATS(playerSquashed,"%d");
SAVE_STATS(shadowSquashed,"%d");
SAVE_STATS(recordTimes,"%d");
SAVE_STATS(switchTimes,"%d");
SAVE_STATS(swapTimes,"%d");
SAVE_STATS(saveTimes,"%d");
SAVE_STATS(loadTimes,"%d");
SAVE_STATS(playTime,"%d");
SAVE_STATS(levelEditTime,"%d");
SAVE_STATS(createdLevels,"%d");
//save achievements.
//format is: name;time,name;time,...
{
vector<string>& v=node.attributes["achievements"];
for(map<string,OwnedAchievement>::iterator it=achievements.begin();it!=achievements.end();++it){
stringstream strm;
char s[32];
long long n=it->second.achievedTime;
sprintf(s,PRINTF_LONGLONG,n);
strm<<it->first<<";"<<s;
v.push_back(strm.str());
}
}
POASerializer serializer;
serializer.writeNode(&node,file,true,true);
}
void StatisticsManager::loadPicture(SDL_Renderer& renderer, ImageManager& imageManager){
//Load drop shadow picture
bmDropShadow=imageManager.loadTexture(getDataPath()+"gfx/dropshadow.png", renderer);
bmQuestionMark=imageManager.loadImage(getDataPath()+"gfx/menu/questionmark.png");
}
void StatisticsManager::registerAchievements(ImageManager& imageManager){
if(!avaliableAchievements.empty()) return;
for(int i=0;achievementList[i].id!=NULL;i++){
avaliableAchievements[achievementList[i].id]=&achievementList[i];
if(achievementList[i].imageFile!=NULL){
achievementList[i].imageSurface = imageManager.loadImage(getDataPath()+achievementList[i].imageFile);
}
}
}
void StatisticsManager::render(ImageManager&,SDL_Renderer &renderer){
if(achievementTime==0 && !bmAchievement && currentAchievement<(int)queuedAchievements.size()){
//create surface
bmAchievement=createAchievementSurface(renderer, queuedAchievements[currentAchievement++]);
//FIXME: Draw the box.
//drawGUIBox(0,0,bmAchievement->w,bmAchievement->h,bmAchievement,0xFFFFFF00);
//check if queue is empty
if(currentAchievement>=(int)queuedAchievements.size()){
queuedAchievements.clear();
currentAchievement=0;
}
//play a sound
getSoundManager()->playSound("achievement", 1, false, 32);
}
//check if we need to display achievements
if(bmAchievement){
achievementTime++;
if(achievementTime<=0){
return;
}else if(achievementTime<=5){
drawAchievement(renderer,achievementTime);
}else if(achievementTime<=achievementDisplayTime-5){
drawAchievement(renderer,5);
}else if(achievementTime<achievementDisplayTime){
drawAchievement(renderer,achievementDisplayTime-achievementTime);
}else if(achievementTime>=achievementIntervalTime){
if(bmAchievement){
bmAchievement.reset();
}
achievementTime=0;
}
}
}
void StatisticsManager::newAchievement(const std::string& id,bool save){
//check avaliable achievements
map<string,AchievementInfo*>::iterator it=avaliableAchievements.find(id);
if(it==avaliableAchievements.end()) return;
//check if already have this achievement
if(save){
map<string,OwnedAchievement>::iterator it2=achievements.find(id);
if(it2!=achievements.end()) return;
OwnedAchievement ach={time(NULL),it->second};
achievements[id]=ach;
}
//add it to queue
queuedAchievements.push_back(it->second);
}
time_t StatisticsManager::achievedTime(const std::string& id) {
auto it = achievements.find(id);
if (it == achievements.end()) return 0;
else return it->second.achievedTime;
}
float StatisticsManager::getAchievementProgress(AchievementInfo* info){
if(!strcmp(info->id,"experienced")){
return float(completedLevels)/50.0f*100.0f;
}
if(!strcmp(info->id,"expert")){
return float(goldLevels)/50.0f*100.0f;
}
if(!strcmp(info->id,"tutorial")){
if(tutorialLevels>0)
return float(tutorialCompleted)/float(tutorialLevels)*100.0f;
else
return 0.0f;
}
if(!strcmp(info->id,"tutorialGold")){
if(tutorialLevels>0)
return float(tutorialGold)/float(tutorialLevels)*100.0f;
else
return 0.0f;
}
if(!strcmp(info->id,"create10")){
return float(createdLevels)/10.0f*100.0f;
}
if (!strcmp(info->id, "jump100")){
return float(playerJumps + shadowJumps) / 100.0f*100.0f;
}
if (!strcmp(info->id, "jump1k")){
return float(playerJumps + shadowJumps) / 1000.0f*100.0f;
}
if(!strcmp(info->id,"die50")){
return float(playerDies+shadowDies)/50.0f*100.0f;
}
if(!strcmp(info->id,"die1000")){
return float(playerDies+shadowDies)/1000.0f*100.0f;
}
if(!strcmp(info->id,"suqash50")){
return float(playerSquashed+shadowSquashed)/50.0f*100.0f;
}
if(!strcmp(info->id,"travel100")){
return (playerTravelingDistance+shadowTravelingDistance)/100.0f*100.0f;
}
if(!strcmp(info->id,"travel1k")){
return (playerTravelingDistance+shadowTravelingDistance)/1000.0f*100.0f;
}
if(!strcmp(info->id,"travel10k")){
return (playerTravelingDistance+shadowTravelingDistance)/10000.0f*100.0f;
}
if(!strcmp(info->id,"travel42k")){
return (playerTravelingDistance+shadowTravelingDistance)/42195.0f*100.0f;
}
if(!strcmp(info->id,"record100")){
return float(recordTimes)/100.0f*100.0f;
}
if(!strcmp(info->id,"record1k")){
return float(recordTimes)/1000.0f*100.0f;
}
if(!strcmp(info->id,"switch100")){
return float(switchTimes)/100.0f*100.0f;
}
if(!strcmp(info->id,"switch1k")){
return float(switchTimes)/1000.0f*100.0f;
}
if(!strcmp(info->id,"swap100")){
return float(swapTimes)/100.0f*100.0f;
}
//not found
return 0.0f;
}
SharedTexture StatisticsManager::createAchievementSurface(SDL_Renderer& renderer, AchievementInfo* info,SDL_Rect* rect,bool showTip,const time_t *achievedTime){
if(info==NULL || info->id==NULL) return NULL;
//prepare text
SurfacePtr title0(nullptr);
SurfacePtr title1(nullptr);
vector<SDL_Surface*> descSurfaces;
SDL_Color fg = objThemes.getTextColor(true);
int fontHeight=TTF_FontLineSkip(fontText);
bool showDescription=false;
bool showImage=false;
float achievementProgress=0.0f;
if(showTip){
title0.reset(TTF_RenderUTF8_Blended(fontText,_("New achievement:"),fg));
title1.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
showDescription=showImage=true;
}else if(achievedTime){
char s[256];
strftime(s,sizeof(s),"%c",localtime(achievedTime));
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
title1.reset(TTF_RenderUTF8_Blended(fontText, tfm::format(_("Achieved on %s"), (char*)s).c_str(), fg));
showDescription=showImage=true;
}else if(info->displayStyle==ACHIEVEMENT_HIDDEN){
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_("Unknown achievement"),fg));
}else{
if(info->displayStyle==ACHIEVEMENT_PROGRESS){
achievementProgress=getAchievementProgress(info);
title1.reset(TTF_RenderUTF8_Blended(fontText, tfm::format(_("Achieved %1.0f%%"), achievementProgress).c_str(), fg));
}else{
title1.reset(TTF_RenderUTF8_Blended(fontText,_("Not achieved"),fg));
}
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
showDescription= info->displayStyle==ACHIEVEMENT_ALL || info->displayStyle==ACHIEVEMENT_PROGRESS;
showImage=true;
}
//calculate the size
int w=0,h=0,w1=8,h1=0;
if(title0!=NULL){
if(title0->w>w) w=title0->w;
h1+=title0->h;
}
if(title1!=NULL){
if(title1->w>w) w=title1->w;
h1+=title1->h;
/*//calc progress bar size
if(!showTip && !achievedTime && info->displayStyle==ACHIEVEMENT_PROGRESS){
h1+=4;
}*/
}
const int preferredImageWidth = 50;
const int preferredImageHeight = 50;
if(showImage){
if(info->imageSurface!=NULL){
// NEW: we have the preferred image size
const int width = std::max(info->r.w, preferredImageWidth);
const int height = std::max(info->r.h, preferredImageHeight);
w1+=width+8;
w+=width+8;
if(height>h1) h1=height;
}
}else{
w1+=bmQuestionMark->w+8;
w+=bmQuestionMark->w+8;
if(bmQuestionMark->h>h1) h1=bmQuestionMark->h;
}
//we render the description here since we need to know the width of title
if (info->description != NULL && showDescription){
string description = _(info->description);
WordWrapper wrapper;
wrapper.font = fontText;
wrapper.maxWidth = showTip ? std::max(int(SCREEN_WIDTH * 0.5f), w) : (rect ? (rect->w - 16) : -1);
wrapper.wordWrap = wrapper.maxWidth > 0;
wrapper.hyphen = "-";
vector<string> lines;
wrapper.addString(lines, description);
int start, end;
const int m = lines.size();
for (start = 0; start < m; start++) {
if (!lines[start].empty()) break;
}
for (end = m - 1; end >= start; end--) {
if (!lines[end].empty()) break;
}
for (int i = start; i <= end; i++) {
if (lines[i].empty()) {
descSurfaces.push_back(TTF_RenderUTF8_Blended(fontText, " ", fg));
} else {
descSurfaces.push_back(TTF_RenderUTF8_Blended(fontText, lines[i].c_str(), fg));
}
}
}
h=h1+8;
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
if(descSurfaces[i]->w>w) w=descSurfaces[i]->w;
}
}
h+=descSurfaces.size()*fontHeight;
w+=16;
h+=16;
//check if size is specified
int left=0,top=0;
if(rect!=NULL){
//NOTE: SDL2 port. This was never used.
/* if(surface!=NULL){
left=rect->x;
top=rect->y;
}*/
if(rect->w>0) w=rect->w;
else rect->w=w;
rect->h=h;
}
//create surface if necessary
SurfacePtr surface = createSurface(w, h);
std::unique_ptr<SDL_Renderer,decltype(&SDL_DestroyRenderer)> surfaceRenderer(
SDL_CreateSoftwareRenderer(surface.get()), &SDL_DestroyRenderer);
//draw background
const SDL_Rect r={left,top,w,h};
if(showTip || achievedTime){
SDL_FillRect(surface.get(),&r,SDL_MapRGB(surface->format,255,255,255));
}else{
SDL_FillRect(surface.get(),&r,SDL_MapRGB(surface->format,192,192,192));
}
//draw horizontal separator
//FIXME: this is moved from StatisticsScreen::createGUI
if (!showTip) {
const SDL_Rect r0 = { left, top, w, 1 };
const SDL_Rect r1 = { left, top + h - 2, w, 1 };
const SDL_Rect r2 = { left, top + h - 1, w, 1 };
Uint32 c0 = achievedTime ? SDL_MapRGB(surface->format, 224, 224, 224) : SDL_MapRGB(surface->format, 168, 168, 168);
Uint32 c2 = achievedTime ? SDL_MapRGB(surface->format, 128, 128, 128) : SDL_MapRGB(surface->format, 96, 96, 96);
SDL_FillRect(surface.get(), &r0, c0);
SDL_FillRect(surface.get(), &r1, c0);
SDL_FillRect(surface.get(), &r2, c2);
}
//draw picture
if(showImage){
if(info->imageSurface){
// NEW: we have the preferred image size
SDL_Rect r={left+8,top+8+(h1-info->r.h)/2,0,0};
if (info->r.w < preferredImageWidth) r.x += (preferredImageWidth - info->r.w) / 2;
SDL_BlitSurface(info->imageSurface,&info->r,surface.get(),&r);
}
}else{
SDL_Rect r={left+8,top+8+(h1-bmQuestionMark->h)/2,0,0};
SDL_BlitSurface(bmQuestionMark,NULL,surface.get(),&r);
}
//draw text
h=8;
if(title0){
SDL_Rect r={left+w1,top+h,0,0};
SDL_BlitSurface(title0.get(),NULL,surface.get(),&r);
h+=title0->h;
}
if(title1){
SDL_Rect r={left+w1,top+h,0,0};
//Draw progress bar.
if(!showTip && !achievedTime && info->displayStyle==ACHIEVEMENT_PROGRESS){
//Draw borders.
SDL_Rect r1={r.x,r.y,w-8-r.x,title1->h};
drawGUIBox(r1.x,r1.y,r1.w,r1.h,*surfaceRenderer,0x1D);
//Draw progress.
r1.x++;
r1.y++;
r1.w=int(achievementProgress/100.0f*float(r1.w-2)+0.5f);
r1.h-=2;
SDL_SetRenderDrawColor(surfaceRenderer.get(),0,0,0,100);
SDL_RenderFillRect(surfaceRenderer.get(),&r1);
//shift the text a little bit (???)
r.x+=2;
r.y+=2;
}
//Draw text.
SDL_BlitSurface(title1.get(),NULL,surface.get(),&r);
}
h=h1+16;
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
SDL_Rect r={left+8,top+h+static_cast<int>(i)*fontHeight,0,0};
SDL_BlitSurface(descSurfaces[i],NULL,surface.get(),&r);
}
}
//clean up
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
SDL_FreeSurface(descSurfaces[i]);
}
}
//FIXME: Should we clear the vector here?
//over
return textureFromSurface(renderer, std::move(surface));
}
void StatisticsManager::drawAchievement(SDL_Renderer& renderer,int alpha){
if(!bmAchievement || alpha<=0) {
return;
}
if(alpha>5) alpha=5;
SDL_Rect r = rectFromTexture(*bmAchievement);
int w=0,h=0;
SDL_GetRendererOutputSize(&renderer, &w, &h);
r.x = w-32-r.w;
r.y = 32;
SDL_SetTextureAlphaMod(bmAchievement.get(), alpha*40);
applyTexture(r.x, r.y,bmAchievement, renderer);
if(!bmDropShadow) {
return;
}
//draw drop shadow - corner
{
int w1=r.w/2,w2=r.w-w1,h1=r.h/2,h2=r.h-h1;
if(w1>16) w1=16;
if(w2>16) w2=16;
if(h1>16) h1=16;
if(h2>16) h2=16;
const int x=(5-alpha)*64;
//top-left
SDL_Rect r1={x,0,w1+16,h1+16};//),r2={r.x-16,r.y-16,0,0};
SDL_Rect r2 ={r.x-16, r.y-16, r1.w, r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//top-right
r1.x=x+48-w2;r2.w=r1.w =w2+16;r2.x=r.x+r.w-w2;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom-right
r1.y=48-h2;r2.h=r1.h=h2+16;r2.y=r.y+r.h-h2;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom-left
r1.x=x;r2.w=r1.w=w1+16;r2.x=r.x-16;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
}
//draw drop shadow - border
int i=r.w-32;
while(i>0){
const int ii=i>128?128:i;
//top
SDL_Rect r1={0,256-alpha*16,ii,16};
SDL_Rect r2={r.x+r.w-16-i,r.y-16,r1.w,r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom
r1.x=128;r2.y=r.y+r.h;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
i-=ii;
}
i=r.h-32;
while(i>0){
const int ii=i>128?128:i;
//top
SDL_Rect r1={512-alpha*16,0,16,ii};
SDL_Rect r2={r.x-16,r.y+r.h-16-i, r1.w, r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom
r1.y=128;r2.x=r.x+r.w;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
i-=ii;
}
}
void StatisticsManager::reloadCompletedLevelsAndAchievements(){
completedLevels=silverLevels=goldLevels=0;
LevelPackManager *lpm=getLevelPackManager();
vector<pair<string,string> > v=lpm->enumLevelPacks();
bool tutorial=false,tutorialIsGold=false;
for(unsigned int i=0;i<v.size();i++){
string& s=v[i].first;
LevelPack *levels=lpm->getLevelPack(s);
levels->loadProgress();
bool b=false;
if(s==lpm->tutorialLevelPackPath){
tutorialLevels=levels->getLevelCount();
tutorialCompleted=tutorialGold=0;
b=tutorial=tutorialIsGold=true;
}
for(int n=0,m=levels->getLevelCount();n<m;n++){
LevelPack::Level *lv=levels->getLevel(n);
int medal=lv->won;
if(medal){
if(lv->targetTime<0 || lv->time<=lv->targetTime)
medal++;
if(lv->targetRecordings<0 || lv->recordings<=lv->targetRecordings)
medal++;
completedLevels++;
if(b) tutorialCompleted++;
if(medal==2) silverLevels++;
if(medal==3){
goldLevels++;
if(b) tutorialGold++;
}
if(medal!=3 && b) tutorialIsGold=false;
}else if(b){
tutorial=tutorialIsGold=false;
}
}
}
//upadte achievements
updateLevelAchievements();
updateTutorialAchievementsInternal((tutorial?1:0)|(tutorialIsGold?2:0));
}
void StatisticsManager::reloadOtherAchievements(){
int i;
if(playTime>=7200) newAchievement("addicted");
if(playTime>=86400) newAchievement("loyalFan");
if(levelEditTime>=7200) newAchievement("constructor");
if(levelEditTime>=28800) newAchievement("constructor2");
if(createdLevels>=1) newAchievement("create1");
if(createdLevels>=10) newAchievement("create10");
i=playerJumps+shadowJumps;
if (i >= 100) newAchievement("jump100");
if (i >= 1000) newAchievement("jump1k");
i=playerDies+shadowDies;
if(i>=1) newAchievement("die1");
if(i>=50) newAchievement("die50");
if(i>=1000) newAchievement("die1000");
i=playerSquashed+shadowSquashed;
if(i>=1) newAchievement("squash1");
if(i>=50) newAchievement("squash50");
float d=playerTravelingDistance+shadowTravelingDistance;
if(d>=100.0f) newAchievement("travel100");
if(d>=1000.0f) newAchievement("travel1k");
if(d>=10000.0f) newAchievement("travel10k");
if(d>=42195.0f) newAchievement("travel42k");
if(recordTimes>=100) newAchievement("record100");
if(recordTimes>=1000) newAchievement("record1k");
if(switchTimes>=100) newAchievement("switch100");
if(switchTimes>=1000) newAchievement("switch1k");
if(swapTimes>=100) newAchievement("swap100");
if(saveTimes>=100) newAchievement("save100");
if(loadTimes>=100) newAchievement("load100");
+ if (collectibleCollected >= 100) newAchievement("collect100");
+ if (collectibleCollected >= 1000) newAchievement("collect1k");
+
if (version.find("Development") != string::npos
|| version.find("Alpha") != string::npos
|| version.find("Beta") != string::npos
|| version.find("RC") != string::npos
|| version.find("Candidate") != string::npos)
{
newAchievement("programmer");
}
}
//Update level specified achievements.
//Make sure the completed level count is correct.
void StatisticsManager::updateLevelAchievements(){
if(completedLevels>=1) newAchievement("newbie");
if(goldLevels>=1) newAchievement("goodjob");
if(completedLevels>=50) newAchievement("experienced");
if(goldLevels>=50) newAchievement("expert");
}
//Update tutorial specified achievements.
//Make sure the level progress of tutorial is correct.
void StatisticsManager::updateTutorialAchievements(){
//find tutorial level pack
LevelPackManager *lpm=getLevelPackManager();
LevelPack *levels=lpm->getTutorialLevelPack();
if(levels==NULL) return;
bool tutorial=true,tutorialIsGold=true;
tutorialLevels=levels->getLevelCount();
tutorialCompleted=tutorialGold=0;
for(int n=0,m=levels->getLevelCount();n<m;n++){
LevelPack::Level *lv=levels->getLevel(n);
int medal=lv->won;
if(medal){
if(lv->targetTime<0 || lv->time<=lv->targetTime)
medal++;
if(lv->targetRecordings<0 || lv->recordings<=lv->targetRecordings)
medal++;
tutorialCompleted++;
if(medal!=3) tutorialIsGold=false;
else tutorialGold++;
}else{
tutorial=tutorialIsGold=false;
break;
}
}
//upadte achievements
updateTutorialAchievementsInternal((tutorial?1:0)|(tutorialIsGold?2:0));
}
//internal function
//flags: a bit-field value indicates which achievements we have.
void StatisticsManager::updateTutorialAchievementsInternal(int flags){
if(flags&1) newAchievement("tutorial");
if(flags&2) newAchievement("tutorialGold");
}
diff --git a/src/StatisticsManager.h b/src/StatisticsManager.h
index 70dfc6d..2c3173b 100644
--- a/src/StatisticsManager.h
+++ b/src/StatisticsManager.h
@@ -1,161 +1,163 @@
/*
* Copyright (C) 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 STATISTICSMANAGER_H
#define STATISTICSMANAGER_H
#include <SDL.h>
#include <string>
#include <vector>
#include <map>
#include <time.h>
#include "Render.h"
#include "AchievementList.h"
struct OwnedAchievement{
time_t achievedTime;
AchievementInfo* info;
};
class StatisticsScreen;
class StatisticsManager{
friend class StatisticsScreen;
public:
//Player and shadow traveling distance (m), 1 block = 1 meter
float playerTravelingDistance,shadowTravelingDistance;
//Player and shadow jumps
int playerJumps,shadowJumps;
//Player and shadow dies
int playerDies,shadowDies;
//Player and shadow squashed
int playerSquashed,shadowSquashed;
//Completed levels. NOTE: this is dynamically calculated, and doesn't save to file.
int completedLevels,silverLevels,goldLevels;
//Record times
int recordTimes;
//number of switched pulled
int switchTimes;
//swap times
int swapTimes;
//save and load times
int saveTimes,loadTimes;
+ //collectible collected
+ int collectibleCollected;
//play time (s)
int playTime;
//level edit time (s)
int levelEditTime;
//created levels
int createdLevels;
private:
//current achievement displayed time
int achievementTime;
//some picture
SharedTexture bmDropShadow;
SDL_Surface* bmQuestionMark;
//SDL_Surface for current achievement (excluding drop shadow)
//SDL_Surface *bmAchievement;
SharedTexture bmAchievement;
//currently owned achievements
std::map<std::string,OwnedAchievement> achievements;
//queued achievements for display
std::vector<AchievementInfo*> queuedAchievements;
//currently displayed achievement
int currentAchievement;
//starting time
time_t startTime;
//level edit starting time
time_t levelEditStartTime;
//statistics for tutorial level pack
int tutorialLevels,tutorialCompleted,tutorialGold;
public:
StatisticsManager();
//clear the statistics and achievements.
void clear();
//load needed picture
void loadPicture(SDL_Renderer &renderer, ImageManager &imageManager);
//register avaliable achievements
static void registerAchievements(ImageManager& imageManager);
//load statistics file.
void loadFile(const std::string& fileName);
//save statistics file.
void saveFile(const std::string& fileName);
//add or display a new achievement.
//name: the achievement id. if can't find it in avaliable achievement, nothing happens.
//save: if true then save to currently owned achievements. if it already exists in
//currently owned achievements, nothing happens.
//if false then just added it to queue, including duplicated achievements.
void newAchievement(const std::string& id,bool save=true);
//if there are new achievements, draw it on the screen,
//otherwise do nothing.
void render(ImageManager&,SDL_Renderer& renderer);
//get the achieved time of an achievement. 0 means not achieved yet.
time_t achievedTime(const std::string& id);
//Call this function to update completed levels.
//NOTE: Level progress files are reloaded, so it's slow.
void reloadCompletedLevelsAndAchievements();
//Call this function to update other achievements at game startup.
void reloadOtherAchievements();
//Update level specified achievements.
//Make sure the completed level count is correct.
void updateLevelAchievements();
//Update tutorial specified achievements.
//Make sure the level progress of tutorial is correct.
void updateTutorialAchievements();
//Call when level edit is start
void startLevelEdit();
//Call when level edit is end
void endLevelEdit();
//update in-game time
void updatePlayTime();
//create a SharedTexture contains specified achievements or draw to existing surface.
//renderer: renderer to create the texture on.
//info: achievement info.
//(surface: specifies SDL_Surface to draw on. if NULL then new surface will be created.)
//NOTE: Removed this arg for sdl2 port as it was not used anyway.
//rect [in, out, optional]: specifies position and optionally width to draw on. height will be returned.
// if NULL then will be drawn on top-left corner. if surface is NULL then rect->x and rect->y are ignored.
//showTip: shows "New achievement" tip
//achievedTime: if we should show achieved time (and progress bar if AchievementInfo specifies) and when is it.
// NOTE: if showTip=true then this argument does nothing.
//return value: A texture that contains the specified achievements or NULL if any error occured.
SharedTexture createAchievementSurface(SDL_Renderer& renderer, AchievementInfo* info,SDL_Rect* rect=NULL,bool showTip=true,const time_t *achievedTime=NULL);
private:
//internal function
//flags: a bit-field value indicates which achievements we have.
void updateTutorialAchievementsInternal(int flags);
//internal function. alpha should be 1-5, 5 means fully opaque (not really)
void drawAchievement(SDL_Renderer& renderer, int alpha);
//internal function for get progress (in percent, 0-100)
float getAchievementProgress(AchievementInfo* info);
};
extern StatisticsManager statsMgr;
#endif
diff --git a/src/StatisticsScreen.cpp b/src/StatisticsScreen.cpp
index 0dd719f..ab74471 100644
--- a/src/StatisticsScreen.cpp
+++ b/src/StatisticsScreen.cpp
@@ -1,424 +1,425 @@
/*
* Copyright (C) 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 <stdio.h>
#include <string>
#include <vector>
#include <map>
#include "StatisticsManager.h"
#include "StatisticsScreen.h"
#include "Globals.h"
#include "Functions.h"
#include "ThemeManager.h"
#include "InputManager.h"
#include "GUIListBox.h"
#include "GUIScrollBar.h"
#include "EasterEggScreen.h"
#include <SDL_ttf_fontfallback.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
//GUI events are handled here.
//name: The name of the element that invoked the event.
//obj: Pointer to the object that invoked the event.
//eventType: Integer containing the type of event.
void StatisticsScreen::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Check what type of event it was.
if(eventType==GUIEventClick){
if(name=="cmdBack"){
//Goto the main menu.
setNextState(STATE_MENU);
}
} else if (eventType == GUIEventChange && name == "lstAchievements") {
//NOTE: This code is only for testing purpose
int index = lists[0]->value;
if (index >= 0) statsMgr.newAchievement(achievementList[index].id, false);
lists[0]->value = -1;
}
}
//Constructor.
StatisticsScreen::StatisticsScreen(ImageManager& imageManager, SDL_Renderer& renderer){
//Update in-game time.
statsMgr.updatePlayTime();
//Render the title.
title = titleTextureFromText(renderer, _("Achievements and Statistics"), objThemes.getTextColor(false), SCREEN_WIDTH);
//Create GUI.
createGUI(imageManager, renderer);
}
//Destructor.
StatisticsScreen::~StatisticsScreen(){
//Delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
//we are so lazy that we just use height of the first text, ignore the others
#define DRAW_PLAYER_STATISTICS(name,var,fmt) { \
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,name,objThemes.getTextColor(true))); \
SurfacePtr stats = createSurface(w,surface->h); \
SDL_FillRect(stats.get(),NULL,-1); \
applySurface(4,0,surface.get(),stats.get(),NULL); \
y=surface->h; \
surface.reset(TTF_RenderUTF8_Blended(fontText, \
tfm::format(fmt,statsMgr.player##var+statsMgr.shadow##var).c_str(), \
objThemes.getTextColor(true))); \
applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
surface.reset(TTF_RenderUTF8_Blended(fontText, \
tfm::format(fmt,statsMgr.player##var).c_str(), \
objThemes.getTextColor(true))); \
applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
surface.reset(TTF_RenderUTF8_Blended(fontText, \
tfm::format(fmt,statsMgr.shadow##var).c_str(), \
objThemes.getTextColor(true))); \
applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats))); /* add it to list box */ \
}
//Add an item to the listbox, that displays "name1", and "var1" formatted with "format"
//we are so lazy that we just use height of the first text, ignore the others
template <class T1>
static void drawMiscStatistics1(SDL_Renderer& renderer, int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1){
//create new surface
SurfacePtr nameSurface(TTF_RenderUTF8_Blended(fontGUISmall,name1,objThemes.getTextColor(true)));
SurfacePtr stats=createSurface(w, nameSurface->h);
SDL_FillRect(stats.get(),NULL,-1);
applySurface(4,0,nameSurface.get(),stats.get(),NULL);
const int x=nameSurface->w+8;
const int y=nameSurface->h;
SurfacePtr formatSurface(TTF_RenderUTF8_Blended(fontText,
tfm::format(format1,var1).c_str(),
objThemes.getTextColor(true)));
//NOTE: SDL2 port. Not halving the y value here as this ends up looking better.
applySurface(x,y-formatSurface->h,formatSurface.get(),stats.get(),NULL);
//add it to list box
list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
//over
//return stats;
}
//NOTE: Disabled this for the SDL2 port for now. It looks a bit off anyhow.
//Might want to make a more general method that draws as many "cells" as there is space.
//Draws two stats on one line if there is space.
//we are so lazy that we just use height of the first text, ignore the others
/*template <class T1,class T2>
static void drawMiscStatistics2(int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1,const char* name2,const T2 var2,const char* format2){
SDL_Surface* stats=drawMiscStatistics1(w,list,name1,var1,format1);
//Check if the width is enough
if(w>=800){
//draw name
SDL_Surface* surface=TTF_RenderUTF8_Blended(fontGUISmall,name2,objThemes.getTextColor(true));
applySurface(w/2-8,stats->h-surface->h,surface,stats,NULL);
int x=surface->w+w/2;
SDL_FreeSurface(surface);
//draw value
char s[1024];
//FIXME: Use tfm::format instead of sprintf to enable locale support
FIXME_sprintf(s,format2,var2);
surface=TTF_RenderUTF8_Blended(fontText,s,objThemes.getTextColor(true));
applySurface(x,(stats->h-surface->h)/2,surface,stats,NULL);
SDL_FreeSurface(surface);
}else{
//Split into two rows
drawMiscStatistics1(w,list,name2,var2,format2);
}
}*/
void StatisticsScreen::addAchievements(ImageManager& imageManager, SDL_Renderer &renderer, GUIListBox *list, bool revealUnknownAchievements) {
for (int idx = 0; achievementList[idx].id != NULL; ++idx) {
time_t *lpt = NULL;
map<string, OwnedAchievement>::iterator it = statsMgr.achievements.find(achievementList[idx].id);
if (it != statsMgr.achievements.end()) {
lpt = &it->second.achievedTime;
}
AchievementInfo info = achievementList[idx];
if (revealUnknownAchievements) {
if (info.displayStyle == ACHIEVEMENT_HIDDEN || info.displayStyle == ACHIEVEMENT_TITLE) {
info.displayStyle = ACHIEVEMENT_ALL;
}
}
SDL_Rect r;
r.x = r.y = 0;
r.w = list->width - 16;
auto surface = statsMgr.createAchievementSurface(renderer, &info, &r, false, lpt);
if (surface){
list->addItem(renderer, "", surface);
}
}
}
//Method that will create the GUI.
void StatisticsScreen::createGUI(ImageManager& imageManager, SDL_Renderer &renderer){
//Create the root element of the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Create back button.
GUIObject* obj=new GUIButton(imageManager,renderer,SCREEN_WIDTH*0.5,SCREEN_HEIGHT-60,-1,36,_("Back"),0,true,true,GUIGravityCenter);
obj->name="cmdBack";
obj->eventCallback=this;
GUIObjectRoot->addChild(obj);
//Create list box.
listBox=new GUISingleLineListBox(imageManager,renderer,(SCREEN_WIDTH-500)/2,104,500,32);
listBox->addItem(_("Achievements"));
listBox->addItem(_("Statistics"));
listBox->value=0;
GUIObjectRoot->addChild(listBox);
//Create list box for achievements.
GUIListBox *list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72);
list->name = "lstAchievements"; // debug only
list->eventCallback = this; // debug only
list->selectable=false;
GUIObjectRoot->addChild(list);
lists.clear();
lists.push_back(list);
addAchievements(imageManager, renderer, list);
//Now create list box for statistics.
list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72,true,false);
list->selectable=false;
GUIObjectRoot->addChild(list);
lists.push_back(list);
//Load needed pictures.
//FIXME: hard-coded image path
//TODO: Might want to consider not caching these as most other stuff use textures now.
SDL_Surface* bmPlayer=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/player.png");
SDL_Surface* bmShadow=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/shadow.png");
SDL_Surface* bmMedal=imageManager.loadImage(getDataPath()+"gfx/medals.png");
SDL_Rect r;
int x,y,w=SCREEN_WIDTH-128;
SharedTexture h_bar = [&](){
//The horizontal bar.
SurfacePtr h_bar(createSurface(w,2));
SDL_Color c = objThemes.getTextColor(true);
Uint32 clr=SDL_MapRGB(h_bar->format,c.r,c.g,c.b);
SDL_FillRect(h_bar.get(),NULL,clr);
return textureFromSurface(renderer, std::move(h_bar));
}();
//Player and shadow specific statistics
//The header.
{
SurfacePtr stats = createSurface(w, 44);
SDL_FillRect(stats.get(),NULL,-1);
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Total"),objThemes.getTextColor(true)));
applySurface(w-260-surface->w,stats->h-surface->h,surface.get(),stats.get(),NULL);
//FIXME: hard-coded player and shadow images
r.x=0;r.y=0;r.w=23;r.h=40;
applySurface(w-140-r.w,stats.get()->h-40,bmPlayer,stats.get(),&r);
applySurface(w-20-r.w,stats.get()->h-40,bmShadow,stats.get(),&r);
list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
}
//Each items.
{
DRAW_PLAYER_STATISTICS(_("Traveling distance (m)"),TravelingDistance,"%0.1f");
DRAW_PLAYER_STATISTICS(_("Jump times"),Jumps,"%d");
DRAW_PLAYER_STATISTICS(_("Die times"),Dies,"%d");
DRAW_PLAYER_STATISTICS(_("Squashed times"),Squashed,"%d");
}
//Game specific statistics.
list->addItem(renderer, "",h_bar);
auto drawMiscStats = [&](const char* name1,const int var1,const char* format1) {
drawMiscStatistics1(renderer, w, list, name1, var1, format1);
};
drawMiscStats(_("Recordings:"),statsMgr.recordTimes,"%d");
drawMiscStats(_("Switch pulled times:"),statsMgr.switchTimes,"%d");
drawMiscStats(_("Swap times:"),statsMgr.swapTimes,"%d");
drawMiscStats(_("Save times:"),statsMgr.saveTimes,"%d");
drawMiscStats(_("Load times:"),statsMgr.loadTimes,"%d");
+ drawMiscStats(_("Collectibles collected:"), statsMgr.collectibleCollected, "%d");
//Level specific statistics
list->addItem(renderer, "",h_bar);
{
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Completed levels:"),objThemes.getTextColor(true)));
SurfacePtr stats = createSurface(w, surface->h);
SDL_FillRect(stats.get(),NULL,-1);
applySurface(4,0,surface.get(),stats.get(),NULL);
x=surface->w+8;
y=surface->h;
surface.reset(TTF_RenderUTF8_Blended(fontText,
tfm::format("%d", statsMgr.completedLevels).c_str(),
objThemes.getTextColor(true)));
applySurface(x,(y-surface->h),surface.get(),stats.get(),NULL);
surface.reset(TTF_RenderUTF8_Blended(fontText,
tfm::format("%d", statsMgr.completedLevels - statsMgr.goldLevels - statsMgr.silverLevels).c_str(),
objThemes.getTextColor(true)));
applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
r.x=0;r.y=0;r.w=30;r.h=30;
applySurface(w-260-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
surface.reset(TTF_RenderUTF8_Blended(fontText,
tfm::format("%d", statsMgr.silverLevels).c_str(),
objThemes.getTextColor(true)));
applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
r.x+=30;
applySurface(w-140-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
surface.reset(TTF_RenderUTF8_Blended(fontText,
tfm::format("%d", statsMgr.goldLevels).c_str(),
objThemes.getTextColor(true)));
applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
r.x+=30;
applySurface(w-20-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats)));
}
//Other statistics.
list->addItem(renderer, "",h_bar);
drawMiscStatistics1(renderer,w,list,_("In-game time:"),
tfm::format("%02d:%02d:%02d", statsMgr.playTime / 3600, (statsMgr.playTime / 60) % 60, statsMgr.playTime % 60),
"%s");
drawMiscStatistics1(renderer,w,list,_("Level editing time:"),
tfm::format("%02d:%02d:%02d", statsMgr.levelEditTime / 3600, (statsMgr.levelEditTime / 60) % 60, statsMgr.levelEditTime % 60),
"%s");
drawMiscStats(_("Created levels:"),statsMgr.createdLevels,"%d");
}
//In this method all the key and mouse events should be handled.
//NOTE: The GUIEvents won't be handled here.
void StatisticsScreen::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if we need to quit, if so enter the exit state.
if(event.type==SDL_QUIT){
setNextState(STATE_EXIT);
}
//Check horizontal movement
int value = listBox->value;
if (inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)){
isKeyboardOnly = true;
value++;
if (value >= (int)listBox->item.size()) value = 0;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_LEFT)){
isKeyboardOnly = true;
value--;
if (value < 0) value = listBox->item.size() - 1;
}
listBox->value = value;
//Check vertical movement
if (value >= 0 && value < (int)lists.size()) {
if (inputMgr.isKeyDownEvent(INPUTMGR_UP)){
isKeyboardOnly = true;
lists[value]->scrollScrollbar(-1);
} else if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN)){
isKeyboardOnly = true;
lists[value]->scrollScrollbar(1);
}
}
//Yet another cheat "ls -la" which reveals all unknown achievements
static char input[6];
static int inputLen = 0;
if (value == 0) {
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym >= 32 && event.key.keysym.sym <= 126) {
if (inputLen < sizeof(input)) input[inputLen] = event.key.keysym.sym;
inputLen++;
} else {
if (event.key.keysym.sym == SDLK_RETURN && inputLen == 6 &&
input[0] == 'l' && input[1] == 's' && input[2] == ' ' && input[3] == '-' && input[4] == 'l' && input[5] == 'a')
{
if (easterEggScreen(imageManager, renderer)) {
//new achievement
statsMgr.newAchievement("cheat");
//reload achievement list with hidden achievements revealed
lists[0]->clearItems();
lists[0]->selectable = true; // debug only
addAchievements(imageManager, renderer, lists[0], true);
}
}
inputLen = 0;
}
}
} else {
inputLen = 0;
}
//Check if the escape button is pressed, if so go back to the main menu.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
setNextState(STATE_MENU);
}
}
//All the logic that needs to be done should go in this method.
void StatisticsScreen::logic(ImageManager&, SDL_Renderer&){
}
//This method handles all the rendering.
void StatisticsScreen::render(ImageManager&, SDL_Renderer& renderer){
//Draw background.
objThemes.getBackground(true)->draw(renderer);
objThemes.getBackground(true)->updateAnimation();
//Draw title.
drawTitleTexture(SCREEN_WIDTH, *title, renderer);
//Draw statistics.
int value=listBox->value;
for(unsigned int i=0;i<lists.size();i++){
lists[i]->visible=(i==value);
}
}
//Method that will be called when the screen size has been changed in runtime.
void StatisticsScreen::resize(ImageManager &imageManager, SDL_Renderer &renderer){
//Recreate the gui to fit the new resolution.
createGUI(imageManager, renderer);
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, May 9, 12:25 AM (1 w, 20 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
62745
Default Alt Text
(108 KB)

Event Timeline