Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
347 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/src/Block.cpp b/src/Block.cpp
index 5d544e1..58e3edd 100644
--- a/src/Block.cpp
+++ b/src/Block.cpp
@@ -1,1211 +1,1200 @@
/*
* 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 "Functions.h"
#include "LevelEditor.h"
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
using namespace std;
Block::Block(Game* parent,int x,int y,int w,int h,int type):
GameObject(parent),
animation(0),
flags(0),
temp(0),
dx(0),
dy(0),
movingPosTime(-1),
speed(0),
objCurrentStand(NULL),
xVel(0), yVel(0),
inAir(false), xVelBase(0), yVelBase(0),
isDelete(false)
{
//Make sure the type is set, if not init should be called somewhere else with this information.
if(type>=0 && type<TYPE_MAX)
init(x,y,w,h,type);
}
Block::Block(const Block& other)
: GameObject(other)
, ScriptProxyUserClass(other)
, animation(other.animation)
, flags(other.flags)
, temp(other.temp)
, dx(other.dx), dy(other.dy)
, movingPos(other.movingPos)
, movingPosTime(-1)
, speed(other.speed)
, objCurrentStand(other.objCurrentStand)
, customAppearanceName(other.customAppearanceName)
, appearance(other.appearance)
, xVel(other.xVel), yVel(other.yVel)
, inAir(other.inAir), xVelBase(other.xVelBase), yVelBase(other.yVelBase)
, id(other.id), destination(other.destination), message(other.message)
, isDelete(other.isDelete)
{
auto se = parent->getScriptExecutor();
if (se == NULL) return;
lua_State *state = se->getLuaState();
if (state == NULL) return;
//Copy the compiledScripts.
for (auto it = other.compiledScripts.begin(); it != other.compiledScripts.end(); ++it) {
lua_rawgeti(state, LUA_REGISTRYINDEX, it->second);
compiledScripts[it->first] = luaL_ref(state, LUA_REGISTRYINDEX);
}
}
Block::~Block() {
auto se = parent->getScriptExecutor();
if (se == NULL) return;
lua_State *state = se->getLuaState();
if (state == NULL) return;
//Delete the compiledScripts.
for (auto it = compiledScripts.begin(); it != compiledScripts.end(); ++it) {
luaL_unref(state, LUA_REGISTRYINDEX, it->second);
}
}
int Block::getPathMaxTime() {
if (movingPosTime < 0) {
movingPosTime = 0;
for (const SDL_Rect& p : movingPos) {
movingPosTime += p.w;
}
}
return movingPosTime;
}
void Block::init(int x,int y,int w,int h,int type){
//First set the location and size of the box.
//The default size is 50x50.
box.x=boxBase.x=x;
box.y=boxBase.y=y;
box.w=boxBase.w=w;
box.h=boxBase.h=h;
//Set the type.
this->type=type;
//Some types need type specific code.
if(type==TYPE_START_PLAYER){
//This is the player start so set the player here.
//We center the player, the player is 23px wide.
parent->player.setLocation(box.x+(box.w-23)/2,box.y);
parent->player.fx=box.x+(box.w-23)/2;
parent->player.fy=box.y;
}else if(type==TYPE_START_SHADOW){
//This is the shadow start so set the shadow here.
//We center the shadow, the shadow is 23px wide.
parent->shadow.setLocation(box.x+(box.w-23)/2,box.y);
parent->shadow.fx=box.x+(box.w-23)/2;
parent->shadow.fy=box.y;
}
objCurrentStand=NULL;
inAir=true;
xVel=yVel=xVelBase=yVelBase=0;
//And load the (default) appearance.
objThemes.getBlock(type)->createInstance(&appearance);
}
void Block::show(SDL_Renderer& renderer){
//Make sure we are visible.
if ((flags & 0x80000000) != 0 && (stateID != STATE_LEVEL_EDITOR || dynamic_cast<LevelEditor*>(parent)->isPlayMode()))
return;
//Check if the block is visible.
if(checkCollision(camera,box)==true || (stateID==STATE_LEVEL_EDITOR && checkCollision(camera,boxBase)==true)){
//Some type of block needs additional state check.
switch(type){
case TYPE_CHECKPOINT:
//Check if the checkpoint is last used.
if(parent!=NULL && parent->objLastCheckPoint.get()==this){
if(!temp) appearance.changeState("activated");
temp=1;
}else{
if(temp) appearance.changeState("default");
temp=0;
}
break;
}
//Always draw the base. (This is only supposed to show in editor.)
appearance.drawState("base", renderer, boxBase.x - camera.x, boxBase.y - camera.y, boxBase.w, boxBase.h);
//What we need to draw depends on the type of block.
switch (type) {
default:
//Draw normal.
appearance.draw(renderer, box.x - camera.x, box.y - camera.y, box.w, box.h);
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
//Draw conveyor belt.
if (animation) {
// FIXME: ad-hoc code. Should add a new animation type in theme system.
const int a = animation / 10;
const SDL_Rect r = { box.x - camera.x, box.y - camera.y, box.w, box.h };
appearance.draw(renderer, box.x - camera.x - 50 + a, box.y - camera.y, box.w + 50, box.h, &r);
} else {
appearance.draw(renderer, box.x - camera.x, box.y - camera.y, box.w, box.h);
}
break;
}
//Some types need to draw something on top of the base/default.
switch(type){
case TYPE_BUTTON:
if(flags&4){
if(animation<5) animation++;
}else{
if(animation>0) animation--;
}
appearance.drawState("button", renderer, box.x - camera.x, box.y - camera.y - 5 + animation, box.w, box.h);
break;
}
//Draw some stupid icons during edit mode.
if (stateID == STATE_LEVEL_EDITOR) {
auto bmGUI = static_cast<LevelEditor*>(parent)->getGuiTexture();
if (!bmGUI) {
return;
}
int x = box.x - camera.x + 2;
//Scripted blocks
if (!scripts.empty() || !compiledScripts.empty()) {
const SDL_Rect r = { 0, 32, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
//Invisible blocks
if (flags & 0x80000000) {
const SDL_Rect r = { 16, 48, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
//Block with custom appearance
if (!customAppearanceName.empty()) {
const SDL_Rect r = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
}
}
}
SDL_Rect Block::getBox(int boxType){
SDL_Rect r={0,0,0,0};
switch(boxType){
case BoxType_Base:
return boxBase;
case BoxType_Previous:
r.x=box.x-dx;
r.y=box.y-dy;
r.w=box.w;
r.h=box.h;
return r;
case BoxType_Delta:
r.x=dx;
r.y=dy;
return r;
case BoxType_Velocity:
r.x=xVel;
r.y=yVel;
//NOTE: In case of the pushable block we sometimes need to substract one from the vertical velocity.
//The yVel is set to one when it's resting, but should be handled as zero in collision.
if(type==TYPE_PUSHABLE && !inAir)
r.y=0;
return r;
case BoxType_Current:
return box;
}
return r;
}
void Block::moveTo(int x,int y){
//The block has moved so calculate the delta.
//NOTE: Every delta is summed since they all happened within one frame and for collision/movement we need the resulting delta.
int delta=(x-box.x);
dx+=delta;
xVel+=delta;
delta=(y-box.y);
dy+=delta;
yVel+=delta;
//And set the new location.
box.x=x;
box.y=y;
}
void Block::growTo(int w,int h){
//The block has changed size
//NOTE: Every delta is summed since they all happened within one frame and for collision/movement we need the resulting delta.
int delta=(w-box.w);
dx+=delta;
xVel+=delta;
delta=(h-box.h);
dy+=delta;
yVel+=delta;
//And set the new location.
box.w=w;
box.h=h;
}
void Block::playAnimation(){
switch(type){
case TYPE_SWAP:
appearance.changeState("activated");
break;
case TYPE_SWITCH:
temp^=1;
appearance.changeState(temp?"activated":"default");
break;
}
}
void Block::onEvent(int eventType){
//Make sure we are visible, otherwise no events should be handled except for 'OnCreate'.
if ((flags & 0x80000000) != 0 && eventType != GameObjectEvent_OnCreate)
return;
//Iterator used to check if the map contains certain entries.
map<int,int>::iterator it;
//Check if there's a script for the event.
it=compiledScripts.find(eventType);
if(it!=compiledScripts.end()){
//There is a script so execute it and check return value.
int ret=parent->getScriptExecutor()->executeScript(it->second,this);
//Return value 1 means do default event process.
//Other values are coming soon...
if(ret!=1) return;
}
//Event handling.
switch(eventType){
case GameObjectEvent_PlayerWalkOn:
switch(type){
case TYPE_FRAGILE:
if ((flags & 0x3) < 3) {
flags++;
const int f = flags & 0x3;
const char* s = (f <= 0) ? "default" : ((f == 1) ? "fragile1" : ((f == 2) ? "fragile2" : "fragile3"));
appearance.changeState(s);
}
break;
}
break;
case GameObjectEvent_PlayerIsOn:
switch(type){
case TYPE_BUTTON:
temp=1;
break;
}
break;
case GameObjectEvent_OnPlayerInteraction:
switch (type) {
case TYPE_SWITCH:
//Make sure that the id isn't emtpy.
if (!id.empty()) {
parent->broadcastObjectEvent(0x10000 | (flags & 3),
-1, id.c_str());
} else {
cerr << "WARNING: invalid switch id!" << endl;
}
break;
}
break;
case GameObjectEvent_OnToggle:
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
flags^=1;
break;
case TYPE_PORTAL:
appearance.changeState("activated");
break;
case TYPE_COLLECTABLE:
appearance.changeState("inactive");
flags|=1;
break;
}
break;
case GameObjectEvent_OnSwitchOn:
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
flags&=~1;
break;
case TYPE_EXIT:
appearance.changeState("default");
break;
}
break;
case GameObjectEvent_OnSwitchOff:
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
flags|=1;
break;
case TYPE_EXIT:
appearance.changeState("closed");
break;
}
break;
}
}
int Block::queryProperties(int propertyType,Player* obj){
switch(propertyType){
case GameObjectProperty_PlayerCanWalkOn:
if (flags & 0x80000000) break;
switch(type){
case TYPE_BLOCK:
case TYPE_MOVING_BLOCK:
case TYPE_CONVEYOR_BELT:
case TYPE_BUTTON:
case TYPE_PUSHABLE:
return 1;
case TYPE_SHADOW_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_SHADOW_CONVEYOR_BELT:
if(obj!=NULL && obj->isShadow()) return 1;
break;
case TYPE_FRAGILE:
if ((flags & 0x3) < 3) return 1;
break;
}
break;
case GameObjectProperty_IsSpikes:
if (flags & 0x80000000) break;
switch(type){
case TYPE_SPIKES:
case TYPE_MOVING_SPIKES:
return 1;
}
break;
case GameObjectProperty_Flags:
return flags;
break;
default:
break;
}
return 0;
}
void Block::getEditorData(std::vector<std::pair<std::string,std::string> >& obj){
//Every block has an id.
obj.push_back(pair<string,string>("id",id));
//And visibility.
obj.push_back(pair<string, string>("visible", (flags & 0x80000000) == 0 ? "1" : "0"));
//And custom appearance.
obj.push_back(pair<string, string>("appearance", customAppearanceName));
//Block specific properties.
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
char s[64],s0[64];
sprintf(s,"%d",(int)movingPos.size());
obj.push_back(pair<string,string>("MovingPosCount",s));
obj.push_back(pair<string,string>("activated",(flags&0x1)?"0":"1"));
obj.push_back(pair<string,string>("loop",(flags&0x2)?"0":"1"));
for(unsigned int i=0;i<movingPos.size();i++){
sprintf(s0+1,"%u",i);
sprintf(s,"%d",movingPos[i].x);
s0[0]='x';
obj.push_back(pair<string,string>(s0,s));
sprintf(s,"%d",movingPos[i].y);
s0[0]='y';
obj.push_back(pair<string,string>(s0,s));
sprintf(s,"%d",movingPos[i].w);
s0[0]='t';
obj.push_back(pair<string,string>(s0,s));
}
}
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
{
char s[64];
obj.push_back(pair<string,string>("activated",(flags&0x1)?"0":"1"));
sprintf(s,"%d",speed);
obj.push_back(pair<string,string>("speed10",s));
}
break;
case TYPE_PORTAL:
obj.push_back(pair<string,string>("automatic",(flags&0x1)?"1":"0"));
obj.push_back(pair<string,string>("destination",destination));
//The message for a portal should not contain '\n'
obj.push_back(pair<string, string>("message", message));
break;
case TYPE_BUTTON:
case TYPE_SWITCH:
{
string s;
switch(flags&0x3){
case 1:
s="on";
break;
case 2:
s="off";
break;
default:
s="toggle";
break;
}
obj.push_back(pair<string,string>("behaviour",s));
if (type == TYPE_SWITCH) {
//The message for a switch should not contain '\n'
obj.push_back(pair<string, string>("message", message));
}
}
break;
case TYPE_NOTIFICATION_BLOCK:
- {
- string value=message;
- //Change \n with the characters '\n'.
- while(value.find('\n',0)!=string::npos){
- size_t pos=value.find('\n',0);
- value=value.replace(pos,1,"\\n");
- }
-
- obj.push_back(pair<string,string>("message",value));
- }
+ //Change \n with the characters '\n'.
+ obj.push_back(pair<string, string>("message", escapeNewline(message)));
break;
case TYPE_FRAGILE:
{
char s[64];
sprintf(s,"%d",flags&0x3);
obj.push_back(pair<string,string>("state",s));
}
break;
}
}
void Block::setEditorData(std::map<std::string,std::string>& obj){
//Iterator used to check if the map contains certain entries.
map<string,string>::iterator it;
//Check if the data contains the appearance.
it = obj.find("appearance");
if (it != obj.end()) {
std::string newAppearanceName;
if (it->second.empty() || it->second == std::string(Game::blockName[type]) + "_Scenery") {
//Use the default appearance. (Do nothing since newAppearanceName is already empty)
} else {
//Use the custom appearance.
newAppearanceName = it->second;
}
if (newAppearanceName != customAppearanceName) {
//Try to find the custom appearance.
ThemeBlock *themeBlock = NULL;
if (!newAppearanceName.empty()) {
themeBlock = objThemes.getScenery(newAppearanceName);
if (themeBlock == NULL) {
std::cerr << "ERROR: failed to load custom appearance '" << newAppearanceName << "' for block " << Game::blockName[type] << std::endl;
}
} else {
themeBlock = objThemes.getBlock(type);
if (themeBlock == NULL) {
std::cerr << "ERROR: failed to load default appearance for block " << Game::blockName[type] << std::endl;
}
}
if (themeBlock) {
//Update the custom appearance name.
customAppearanceName = newAppearanceName;
//Recreate the theme block instance.
themeBlock->createInstance(&appearance);
//Do some block specific stuff,
//e.g. reset the state according to block type,
//or load some missing part of block states from default appearance.
switch (type) {
case TYPE_FRAGILE:
{
const int f = flags & 0x3;
const char* s = (f == 0) ? "default" : ((f == 1) ? "fragile1" : ((f == 2) ? "fragile2" : "fragile3"));
appearance.changeState(s);
}
break;
case TYPE_BUTTON:
if (appearance.blockStates.find("button") == appearance.blockStates.end()) {
//Try to load the "button" state from default appearance
ThemeBlockInstance defaultAppearance;
objThemes.getBlock(type)->createInstance(&defaultAppearance);
auto it = defaultAppearance.blockStates.find("button");
if (it != defaultAppearance.blockStates.end() && it->second >= 0 && it->second < (int)defaultAppearance.states.size()) {
appearance.states.push_back(defaultAppearance.states[it->second]);
appearance.blockStates[it->first] = appearance.states.size() - 1;
}
}
break;
}
}
}
}
//Check if the data contains the id block.
it=obj.find("id");
if(it!=obj.end()){
//Set the id of the block.
id=obj["id"];
}
//Check if the data contains the visibility
it = obj.find("visible");
if (it != obj.end()) {
//Set the visibility.
const string& s = it->second;
flags = (flags & ~0x80000000) | ((s == "true" || atoi(s.c_str())) ? 0 : 0x80000000);
}
//Block specific properties.
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
//Make sure that the editor data contains MovingPosCount.
it=obj.find("MovingPosCount");
if(it!=obj.end()){
char s0[64];
int m=atoi(obj["MovingPosCount"].c_str());
movingPos.clear();
for(int i=0;i<m;i++){
SDL_Rect r={0,0,0,0};
sprintf(s0+1,"%d",i);
s0[0]='x';
r.x=atoi(obj[s0].c_str());
s0[0]='y';
r.y=atoi(obj[s0].c_str());
s0[0]='t';
r.w=atoi(obj[s0].c_str());
movingPos.push_back(r);
}
}
//Check if the activated or disabled key is in the data.
//NOTE: 'disabled' is obsolete in V0.5.
it=obj.find("activated");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(!(s=="true" || atoi(s.c_str()))) flags|=0x1;
}else{
it=obj.find("disabled");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(s=="true" || atoi(s.c_str())) flags|=0x1;
}
}
//Check if the loop key is in the data.
it=obj.find("loop");
if(it!=obj.end()){
const string& s=it->second;
flags |= 0x2;
if (s == "true" || atoi(s.c_str())) flags &= ~0x2;
}
}
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
{
//Check if there's a speed key in the editor data.
//NOTE: 'speed' is obsolete in V0.5.
it=obj.find("speed10");
if(it!=obj.end()){
speed=atoi(it->second.c_str());
}else{
it = obj.find("speed");
if (it != obj.end()){
speed = atoi(it->second.c_str()) * 10;
}
}
//Check if the activated or disabled key is in the data.
//NOTE: 'disabled' is obsolete in V0.5.
it=obj.find("activated");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(!(s=="true" || atoi(s.c_str()))) flags|=0x1;
}else{
it=obj.find("disabled");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(s=="true" || atoi(s.c_str())) flags|=0x1;
}
}
}
break;
case TYPE_PORTAL:
{
//Check if the automatic key is in the data.
it=obj.find("automatic");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(s=="true" || atoi(s.c_str())) flags|=0x1;
}
//Check if the destination key is in the data.
it=obj.find("destination");
if(it!=obj.end()){
destination=it->second;
}
if ((it = obj.find("message")) != obj.end()) {
//The message for a portal should not contain '\n'
message = it->second;
}
}
break;
case TYPE_BUTTON:
case TYPE_SWITCH:
{
//Check if the behaviour key is in the data.
it=obj.find("behaviour");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x3;
if(s=="on" || s==_("On")) flags|=1;
else if(s=="off" || s==_("Off")) flags|=2;
}
if (type == TYPE_SWITCH && (it = obj.find("message")) != obj.end()) {
//The message for a switch should not contain '\n'
message = it->second;
}
}
break;
case TYPE_NOTIFICATION_BLOCK:
{
//Check if the message key is in the data.
it=obj.find("message");
if(it!=obj.end()){
- message=it->second;
//Change the characters '\n' to a real \n
- while(message.find("\\n")!=string::npos){
- message=message.replace(message.find("\\n"),2,"\n");
- }
+ message = unescapeNewline(it->second);
}
}
break;
case TYPE_FRAGILE:
{
//Check if the status is in the data.
it=obj.find("state");
if(it!=obj.end()){
flags=(flags&~0x3)|(atoi(it->second.c_str())&0x3);
const int f = flags & 0x3;
const char* s=(f==0)?"default":((f==1)?"fragile1":((f==2)?"fragile2":"fragile3"));
appearance.changeState(s);
}
}
break;
}
}
std::string Block::getEditorProperty(const std::string& property){
//First get the complete editor data.
vector<pair<string,string> > objMap;
vector<pair<string,string> >::iterator it;
getEditorData(objMap);
//Loop through the entries.
for(it=objMap.begin();it!=objMap.end();++it){
if(it->first==property)
return it->second;
}
//Nothing found.
return "";
}
void Block::setEditorProperty(const std::string& property, const std::string& value){
//Create a map to hold the property.
std::map<std::string,std::string> editorData;
editorData[property]=value;
//And call the setEditorData method.
setEditorData(editorData);
}
bool Block::loadFromNode(ImageManager&, SDL_Renderer&, TreeStorageNode* objNode){
//Make sure there are enough parameters.
if(objNode->value.size()<3)
return false;
//Load the type and location.
int type = 0;
{
auto it = Game::blockNameMap.find(objNode->value[0]);
if (it != Game::blockNameMap.end()) {
type = it->second;
} else {
cerr << "WARNING: Unknown block type '" << objNode->value[0] << "'!" << endl;
}
}
int x=atoi(objNode->value[1].c_str());
int y=atoi(objNode->value[2].c_str());
int w=50;
int h=50;
if(objNode->value.size()>3)
w=atoi(objNode->value[3].c_str());
if(objNode->value.size()>4)
h=atoi(objNode->value[4].c_str());
//Call the init method.
init(x,y,w,h,type);
//Loop through the attributes as editorProperties.
map<string,string> obj;
for(map<string,vector<string> >::iterator i=objNode->attributes.begin();i!=objNode->attributes.end();++i){
if(i->second.size()>0) obj[i->first]=i->second[0];
}
setEditorData(obj);
//Loop through the subNodes.
for(unsigned int i=0;i<objNode->subNodes.size();i++){
//FIXME: Ugly variable naming.
TreeStorageNode* obj=objNode->subNodes[i];
if(obj==NULL) continue;
//Check for a script block.
if(obj->name=="script" && !obj->value.empty()){
map<string,int>::iterator it=Game::gameObjectEventNameMap.find(obj->value[0]);
if(it!=Game::gameObjectEventNameMap.end()){
int eventType=it->second;
const std::string& script=obj->attributes["script"][0];
if(!script.empty()) scripts[eventType]=script;
}
}
}
return true;
}
void Block::move(){
bool isPlayMode = stateID != STATE_LEVEL_EDITOR || dynamic_cast<LevelEditor*>(parent)->isPlayMode();
//Make sure we are visible, if not return.
if ((flags & 0x80000000) != 0 && isPlayMode)
return;
//First update the animation of the appearance.
appearance.updateAnimation();
//Block specific move code.
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
//Only move block when we are in play mode.
if (isPlayMode) {
//Make sure the block is enabled, if so increase the time.
if(!(flags&0x1)) temp++;
int t=temp;
SDL_Rect r0={0,0,0,0},r1;
dx=0;
dy=0;
//Loop through the moving positions.
for(unsigned int i=0;i<movingPos.size();i++){
r1.x=movingPos[i].x;
r1.y=movingPos[i].y;
r1.w=movingPos[i].w;
if(t==0&&r1.w==0){ // time == 0 means the block deactivates at this point automatically
r1.w=1;
flags|=0x1;
}
if(t>=0 && t<(int)r1.w){
int newX=boxBase.x+(int)(float(r0.x)+(float(r1.x)-float(r0.x))*float(t)/float(r1.w));
int newY=boxBase.y+(int)(float(r0.y)+(float(r1.y)-float(r0.y))*float(t)/float(r1.w));
//Calculate the delta and velocity.
xVel=dx=newX-box.x;
yVel=dy=newY-box.y;
//Set the new location of the moving block.
box.x=newX;
box.y=newY;
return;
} else if (t == (int)r1.w && i == movingPos.size() - 1) {
//If the time is the time of the movingPosition then set it equal to the location.
//We do this to prevent a slight edge between normal blocks and moving blocks.
int newX=boxBase.x+r1.x;
int newY=boxBase.y+r1.y;
xVel=dx=newX-box.x;
yVel=dy=newY-box.y;
box.x=newX;
box.y=newY;
return;
}
t-=r1.w;
r0.x=r1.x;
r0.y=r1.y;
}
//Only reset the stuff when we're looping.
if((flags & 0x2) == 0){
//Set the time back to zero.
temp=0;
//Calculate the delta movement.
if(!movingPos.empty() && movingPos.back().x==0 && movingPos.back().y==0){
dx=boxBase.x-box.x;
dy=boxBase.y-box.y;
}
//Set the movingblock back to it's initial location.
box.x=boxBase.x;
box.y=boxBase.y;
} else {
//Reached the end, but not looping
xVel=yVel=dx=dy=0;
}
}
break;
case TYPE_BUTTON:
//Only move block when we are in play mode.
if (isPlayMode) {
//Check the third bit of flags to see if temp changed.
int new_flags=temp?4:0;
if((flags^new_flags)&4){
//The button has been pressed or unpressed so change the third bit on flags.
flags=(flags&~4)|new_flags;
if(parent && (new_flags || (flags&3)==0)){
//Make sure that id isn't empty.
if(!id.empty()){
parent->broadcastObjectEvent(0x10000|(flags&3),-1,id.c_str());
}else{
cerr<<"WARNING: invalid button id!"<<endl;
}
}
}
temp=0;
}
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
//NOTE: we update conveyor belt animation even in edit mode.
//Increase the conveyor belt animation.
if((flags&1)==0){
//Since now 1 speed = 0.1 pixel/s we need some more sophisticated calculation.
int a = animation + speed, d = 0;
if (a < 0) {
//Add a delta value to make it positive
d = (((-a) / 500) + 1) * 500;
}
//Set the velocity NOTE This isn't the actual velocity of the block, but the speed of the player/shadow standing on it.
xVel = (a + d) / 10 - (animation + d) / 10;
//Update animation value
animation = (a + d) % 500;
assert(animation >= 0);
} else {
//Clear the velocity NOTE This isn't the actual velocity of the block, but the speed of the player/shadow standing on it.
xVel = 0;
}
break;
case TYPE_PUSHABLE:
//Only move block when we are in play mode.
if (isPlayMode) {
//Update the vertical velocity, horizontal is set by the player.
if(inAir==true){
yVel+=1;
//Cap fall speed to 13.
if(yVel>13)
yVel=13;
}
if (auto ocs = objCurrentStand.get()) {
//Now get the velocity and delta of the object the player is standing on.
SDL_Rect v=ocs->getBox(BoxType_Velocity);
SDL_Rect delta=ocs->getBox(BoxType_Delta);
switch(ocs->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)
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(auto o : parent->levelObjects){
//Make sure to only check visible blocks.
if(o->flags & 0x80000000)
continue;
//Make sure we aren't the block.
if(o==this)
continue;
//Make sure the object is spike or solid for the player.
if(o->type!=TYPE_SPIKES && o->type!=TYPE_MOVING_SPIKES && !o->queryProperties(GameObjectProperty_PlayerCanWalkOn,&parent->player))
continue;
//Check for collision.
if(checkCollision(box,o->getBox()))
objects.push_back(o);
}
//There was collision so try to resolve it.
if(!objects.empty()){
//FIXME: When multiple moving blocks are overlapping the pushable can be "bounced" off depending on the block order.
for(auto o : objects){
SDL_Rect r=o->getBox();
SDL_Rect delta=o->getBox(BoxType_Delta);
//Check on which side of the box the pushable is.
if(delta.x!=0){
if(delta.x>0){
//Move the pushable 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 pushable 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 pushable 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 pushable up if necessary.
if((box.y+box.h)-r.y<=-delta.y && box.y>r.y-box.h)
box.y=r.y-box.h;
}
}
}
}
//Reuse the objects array, this time for blocks the block moves into.
objects.clear();
//Determine the collision frame.
SDL_Rect frame={box.x,box.y,box.w,box.h};
//Keep the horizontal movement of the block 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(auto o : parent->levelObjects){
//Make sure the object is visible.
if(o->flags & 0x80000000)
continue;
//Make sure we aren't the block.
if(o==this)
continue;
//Check if the player can collide with this game object, or this object is spike.
if(o->type!=TYPE_SPIKES && o->type!=TYPE_MOVING_SPIKES && !o->queryProperties(GameObjectProperty_PlayerCanWalkOn,&parent->player))
continue;
//Check if the block is inside the frame.
if(checkCollision(frame,o->getBox()))
objects.push_back(o);
}
//Horizontal pass.
if(xVel+xVelBase!=0){
box.x+=xVel+xVelBase;
for(auto o : objects){
SDL_Rect r=o->getBox();
if(!checkCollision(box,r))
continue;
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;
for(auto o : objects){
SDL_Rect r=o->getBox();
if(!checkCollision(box,r))
continue;
//Now check how we entered the block (vertically or horizontally).
if(yVel+yVelBase>0){
//We came from the top so the bottom edge of the player must be less or equal than yVel+yVelBase.
if((box.y+box.h)-r.y<=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){
//There is one, so check 'how much' the player is on the blocks.
SDL_Rect r=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=o->getBox(BoxType_Velocity);
SDL_Rect v2=lastStand->getBox(BoxType_Velocity);
if(v.y==v2.y){
if(w>w2)
lastStand=o;
}else if(v.y<v2.y){
lastStand=o;
}
}else{
lastStand=o;
}
}
}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;
}
//Block will currently be standing on whatever it was last standing on.
objCurrentStand=lastStand;
dx=box.x-lastX;
dy=box.y-lastY;
xVel=0;
xVelBase=0;
}
break;
}
}
void Block::deleteMe() {
if (isDelete) return;
//Set the delete flag.
isDelete = true;
//Hide this block.
flags |= 0x80000000;
//Invalidate all references to this block.
if (proxy->object == this) {
proxy->object = NULL;
}
//Update totalCollectables, etc. when removing a collectable
if (type == TYPE_COLLECTABLE) {
if (flags & 0x1) parent->currentCollectables--;
parent->totalCollectables--;
}
}
diff --git a/src/Functions.cpp b/src/Functions.cpp
index cdcfcaf..4075ce6 100644
--- a/src/Functions.cpp
+++ b/src/Functions.cpp
@@ -1,1631 +1,1679 @@
/*
* 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 <stdio.h>
#include <math.h>
#include <string.h>
#include <algorithm>
#include <SDL.h>
#include <SDL_mixer.h>
#include <SDL_syswm.h>
#include <SDL_ttf.h>
#include <string>
#include "Globals.h"
#include "Functions.h"
#include "FontManager.h"
#include "FileManager.h"
#include "GameObjects.h"
#include "LevelPack.h"
#include "TitleMenu.h"
#include "OptionsMenu.h"
#include "CreditsMenu.h"
#include "LevelEditSelect.h"
#include "LevelEditor.h"
#include "Game.h"
#include "LevelPlaySelect.h"
#include "Addons.h"
#include "InputManager.h"
#include "ImageManager.h"
#include "MusicManager.h"
#include "SoundManager.h"
#include "ScriptExecutor.h"
#include "LevelPackManager.h"
#include "ThemeManager.h"
#include "GUIListBox.h"
#include "GUIOverlay.h"
#include "StatisticsManager.h"
#include "StatisticsScreen.h"
#include "Cursors.h"
#include "ScriptAPI.h"
#include "libs/tinyformat/tinyformat.h"
#include "libs/tinygettext/tinygettext.hpp"
#include "libs/tinygettext/log.hpp"
#include "libs/findlocale/findlocale.h"
using namespace std;
#ifdef WIN32
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#define TO_UTF8(SRC, DEST) WideCharToMultiByte(CP_UTF8, 0, SRC, -1, DEST, sizeof(DEST), NULL, NULL)
#define TO_UTF16(SRC, DEST) MultiByteToWideChar(CP_UTF8, 0, SRC, -1, DEST, sizeof(DEST)/sizeof(DEST[0]))
#else
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>
#endif
//Initialise the musicManager.
//The MusicManager is used to prevent loading music files multiple times and for playing/fading music.
MusicManager musicManager;
//Initialise the soundManager.
//The SoundManager is used to keep track of the sfx in the game.
SoundManager soundManager;
//Initialise the levelPackManager.
//The LevelPackManager is used to prevent loading levelpacks multiple times and for the game to know which levelpacks there are.
LevelPackManager levelPackManager;
//Map containing changed settings using command line arguments.
map<string,string> tmpSettings;
//Pointer to the settings object.
//It is used to load and save the settings file and change the settings.
Settings* settings=nullptr;
SDL_Renderer* sdlRenderer=nullptr;
std::string pgettext(const std::string& context, const std::string& message) {
if (dictionaryManager) {
return dictionaryManager->get_dictionary().translate_ctxt(context, message);
} else {
return message;
}
}
std::string ngettext(const std::string& message,const std::string& messageplural,int num) {
if (dictionaryManager) {
return dictionaryManager->get_dictionary().translate_plural(message, messageplural, num);
} else {
//Assume it's of English plural rule
return (num != 1) ? messageplural : message;
}
}
void applySurface(int x,int y,SDL_Surface* source,SDL_Surface* dest,SDL_Rect* clip){
//The offset is needed to draw at the right location.
SDL_Rect offset;
offset.x=x;
offset.y=y;
//Let SDL do the drawing of the surface.
SDL_BlitSurface(source,clip,dest,&offset);
}
void drawRect(int x,int y,int w,int h,SDL_Renderer& renderer,Uint32 color){
//NOTE: We let SDL_gfx render it.
SDL_SetRenderDrawColor(&renderer,color >> 24,color >> 16,color >> 8,255);
//rectangleRGBA(&renderer,x,y,x+w,y+h,color >> 24,color >> 16,color >> 8,255);
const SDL_Rect r{x,y,w,h};
SDL_RenderDrawRect(&renderer,&r);
}
//Draw a box with anti-aliased borders using SDL_gfx.
void drawGUIBox(int x,int y,int w,int h,SDL_Renderer& renderer,Uint32 color){
SDL_Renderer* rd = &renderer;
//FIXME, this may get the wrong color on system with different endianness.
//Fill content's background color from function parameter
SDL_SetRenderDrawColor(rd,color >> 24,color >> 16,color >> 8,color >> 0);
{
const SDL_Rect r{x+1,y+1,w-2,h-2};
SDL_RenderFillRect(rd, &r);
}
SDL_SetRenderDrawColor(rd,0,0,0,255);
//Draw first black borders around content and leave 1 pixel in every corner
SDL_RenderDrawLine(rd,x+1,y,x+w-2,y);
SDL_RenderDrawLine(rd,x+1,y+h-1,x+w-2,y+h-1);
SDL_RenderDrawLine(rd,x,y+1,x,y+h-2);
SDL_RenderDrawLine(rd,x+w-1,y+1,x+w-1,y+h-2);
//Fill the corners with transperent color to create anti-aliased borders
SDL_SetRenderDrawColor(rd,0,0,0,160);
SDL_RenderDrawPoint(rd,x,y);
SDL_RenderDrawPoint(rd,x,y+h-1);
SDL_RenderDrawPoint(rd,x+w-1,y);
SDL_RenderDrawPoint(rd,x+w-1,y+h-1);
//Draw second lighter border around content
SDL_SetRenderDrawColor(rd,0,0,0,64);
{
const SDL_Rect r{x+1,y+1,w-2,h-2};
SDL_RenderDrawRect(rd,&r);
}
SDL_SetRenderDrawColor(rd,0,0,0,50);
//Create anti-aliasing in corners of second border
SDL_RenderDrawPoint(rd,x+1,y+1);
SDL_RenderDrawPoint(rd,x+1,y+h-2);
SDL_RenderDrawPoint(rd,x+w-2,y+1);
SDL_RenderDrawPoint(rd,x+w-2,y+h-2);
}
void drawLine(int x1,int y1,int x2,int y2,SDL_Renderer& renderer,Uint32 color){
SDL_SetRenderDrawColor(&renderer,color >> 24,color >> 16,color >> 8,255);
//NOTE: We let SDL_gfx render it.
//lineRGBA(&renderer,x1,y1,x2,y2,color >> 24,color >> 16,color >> 8,255);
SDL_RenderDrawLine(&renderer,x1,y1,x2,y2);
}
void drawLineWithArrow(int x1,int y1,int x2,int y2,SDL_Renderer& renderer,Uint32 color,int spacing,int offset,int xsize,int ysize){
//Draw line first
drawLine(x1,y1,x2,y2,renderer,color);
//calc delta and length
double dx=x2-x1;
double dy=y2-y1;
double length=sqrt(dx*dx+dy*dy);
if(length<0.001) return;
//calc the unit vector
dx/=length; dy/=length;
//Now draw arrows on it
for(double p=offset;p<length;p+=spacing){
drawLine(int(x1+p*dx+0.5),int(y1+p*dy+0.5),
int(x1+(p-xsize)*dx-ysize*dy+0.5),int(y1+(p-xsize)*dy+ysize*dx+0.5),renderer,color);
drawLine(int(x1+p*dx+0.5),int(y1+p*dy+0.5),
int(x1+(p-xsize)*dx+ysize*dy+0.5),int(y1+(p-xsize)*dy-ysize*dx+0.5),renderer,color);
}
}
ScreenData creationFailed() {
return ScreenData{ nullptr };
}
ScreenData createScreen(){
//Check if we are going fullscreen.
if(settings->getBoolValue("fullscreen"))
pickFullscreenResolution();
//Set the screen_width and height.
SCREEN_WIDTH=atoi(settings->getValue("width").c_str());
SCREEN_HEIGHT=atoi(settings->getValue("height").c_str());
//Update the camera.
camera.w=SCREEN_WIDTH;
camera.h=SCREEN_HEIGHT;
//Set the flags.
Uint32 flags = 0;
Uint32 currentFlags = SDL_GetWindowFlags(sdlWindow);
//#if !defined(ANDROID)
// flags |= SDL_DOUBLEBUF;
//#endif
if(settings->getBoolValue("fullscreen")) {
flags|=SDL_WINDOW_FULLSCREEN; //TODO with SDL2 we can also do SDL_WINDOW_FULLSCREEN_DESKTOP
}
else if(settings->getBoolValue("resizable"))
flags|=SDL_WINDOW_RESIZABLE;
//Create the window and renderer if they don't exist and check if there weren't any errors.
if (!sdlWindow && !sdlRenderer) {
SDL_CreateWindowAndRenderer(SCREEN_WIDTH, SCREEN_HEIGHT, flags, &sdlWindow, &sdlRenderer);
if(!sdlWindow || !sdlRenderer){
std::cerr << "FATAL ERROR: SDL_CreateWindowAndRenderer failed.\nError: " << SDL_GetError() << std::endl;
return creationFailed();
}
SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BlendMode::SDL_BLENDMODE_BLEND);
// White background so we see the menu on failure.
SDL_SetRenderDrawColor(sdlRenderer, 255, 255, 255, 255);
} else if (sdlWindow) {
// Try changing to/from fullscreen
if(SDL_SetWindowFullscreen(sdlWindow, flags & SDL_WINDOW_FULLSCREEN) != 0) {
std::cerr << "WARNING: Failed to switch to fullscreen: " << SDL_GetError() << std::endl;
};
currentFlags = SDL_GetWindowFlags(sdlWindow);
// Change fullscreen resolution
if((currentFlags & SDL_WINDOW_FULLSCREEN ) || (currentFlags & SDL_WINDOW_FULLSCREEN_DESKTOP)) {
SDL_DisplayMode m{0,0,0,0,nullptr};
SDL_GetWindowDisplayMode(sdlWindow,&m);
m.w = SCREEN_WIDTH;
m.h = SCREEN_HEIGHT;
if(SDL_SetWindowDisplayMode(sdlWindow, &m) != 0) {
std::cerr << "WARNING: Failed to set display mode: " << SDL_GetError() << std::endl;
}
} else {
SDL_SetWindowSize(sdlWindow, SCREEN_WIDTH, SCREEN_HEIGHT);
}
}
//Now configure the newly created window (if windowed).
if(settings->getBoolValue("fullscreen")==false)
configureWindow();
//Set the the window caption.
SDL_SetWindowTitle(sdlWindow, ("Me and My Shadow "+version).c_str());
//FIXME Seems to be obsolete
// SDL_EnableUNICODE(1);
//Nothing went wrong so return true.
return ScreenData{sdlRenderer};
}
vector<SDL_Point> getResolutionList(){
//Vector that will hold the resolutions to choose from.
vector<SDL_Point> resolutionList;
//Enumerate available resolutions using SDL_ListModes()
//NOTE: we enumerate fullscreen resolutions because
// windowed resolutions always can be arbitrary
if(resolutionList.empty()){
// SDL_Rect **modes=SDL_ListModes(NULL,SDL_FULLSCREEN|SCREEN_FLAGS|SDL_ANYFORMAT);
//NOTe - currently only using the first display (0)
int numDisplayModes = SDL_GetNumDisplayModes(0);
if(numDisplayModes < 1){
cerr<<"ERROR: Can't enumerate available screen resolutions."
" Use predefined screen resolutions list instead."<<endl;
static const SDL_Point predefinedResolutionList[] = {
{800,600},
{1024,600},
{1024,768},
{1152,864},
{1280,720},
{1280,768},
{1280,800},
{1280,960},
{1280,1024},
{1360,768},
{1366,768},
{1440,900},
{1600,900},
{1600,1200},
{1680,1080},
{1920,1080},
{1920,1200},
{2560,1440},
{3840,2160}
};
//Fill the resolutionList.
for (unsigned int i = 0; i<sizeof(predefinedResolutionList) / sizeof(SDL_Point); i++){
resolutionList.push_back(predefinedResolutionList[i]);
}
}else{
//Fill the resolutionList.
for(int i=0;i < numDisplayModes; ++i){
SDL_DisplayMode mode;
int error = SDL_GetDisplayMode(0, i, &mode);
if(error < 0) {
//We failed to get a display mode. Should we crash here?
std::cerr << "ERROR: Failed to get display mode " << i << " " << std::endl;
}
//Check if the resolution is higher than the minimum (800x600).
if(mode.w >= 800 && mode.h >= 600){
SDL_Point res = { mode.w, mode.h };
resolutionList.push_back(res);
}
}
//Reverse it so that we begin with the lowest resolution.
reverse(resolutionList.begin(),resolutionList.end());
}
}
//Return the resolution list.
return resolutionList;
}
void pickFullscreenResolution(){
//Get the resolution list.
vector<SDL_Point> resolutionList=getResolutionList();
//The resolution that will hold the final result, we start with the minimum (800x600).
SDL_Point closestMatch = { 800, 600 };
int width=atoi(getSettings()->getValue("width").c_str());
int height=atoi(getSettings()->getValue("height").c_str());
int delta = 0x40000000;
//Now loop through the resolutionList.
for (int i = 0; i < (int)resolutionList.size(); i++){
int dx = width - resolutionList[i].x;
if (dx < 0) dx = -dx;
int dy = height - resolutionList[i].y;
if (dy < 0) dy = -dy;
if (dx + dy < delta){
delta = dx + dy;
closestMatch.x = resolutionList[i].x;
closestMatch.y = resolutionList[i].y;
}
}
//Now set the resolution to the closest match.
char s[64];
sprintf(s,"%d",closestMatch.x);
getSettings()->setValue("width",s);
sprintf(s,"%d",closestMatch.y);
getSettings()->setValue("height",s);
}
void configureWindow(){
//We only need to configure the window if it's resizable.
if(!getSettings()->getBoolValue("resizable"))
return;
//We use a new function in SDL2 to restrict minimum window size
SDL_SetWindowMinimumSize(sdlWindow, 800, 600);
}
void onVideoResize(ImageManager& imageManager, SDL_Renderer &renderer){
//Check if the resize event isn't malformed.
if(event.window.data1<=0 || event.window.data2<=0)
return;
//Check the size limit.
//TODO: SDL2 porting note: This may break on systems non-X11 or Windows systems as the window size won't be limited
//there.
if(event.window.data1<800)
event.window.data1=800;
if(event.window.data2<600)
event.window.data2=600;
//Check if it really resizes.
if(SCREEN_WIDTH==event.window.data1 && SCREEN_HEIGHT==event.window.data2)
return;
char s[32];
//Set the new width and height.
SDL_snprintf(s,32,"%d",event.window.data1);
getSettings()->setValue("width",s);
SDL_snprintf(s,32,"%d",event.window.data2);
getSettings()->setValue("height",s);
//FIXME: THIS doesn't work properly.
//Do resizing.
SCREEN_WIDTH = event.window.data1;
SCREEN_HEIGHT = event.window.data2;
//Update the camera.
camera.w=SCREEN_WIDTH;
camera.h=SCREEN_HEIGHT;
//Tell the theme to resize.
if(!loadTheme(imageManager,renderer,""))
return;
//And let the currentState update it's GUI to the new resolution.
currentState->resize(imageManager, renderer);
}
ScreenData init(){
//Initialze SDL.
if(SDL_Init(SDL_INIT_TIMER|SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_JOYSTICK)==-1) {
std::cerr << "FATAL ERROR: SDL_Init failed\nError: " << SDL_GetError() << std::endl;
return creationFailed();
}
//Initialze SDL_mixer (audio).
//Note for SDL2 port: Changed frequency from 22050 to 44100.
//22050 caused some sound artifacts on my system, and I'm not sure
//why one would use it in this day and age anyhow.
//unless it's for compatability with some legacy system.
if(Mix_OpenAudio(44100,MIX_DEFAULT_FORMAT,2,1024)==-1){
std::cerr << "FATAL ERROR: Mix_OpenAudio failed\nError: " << Mix_GetError() << std::endl;
return creationFailed();
}
//Set the volume.
Mix_Volume(-1,atoi(settings->getValue("sound").c_str()));
//Increase the number of channels.
soundManager.setNumberOfChannels(48);
//Initialze SDL_ttf (fonts).
if(TTF_Init()==-1){
std::cerr << "FATAL ERROR: TTF_Init failed\nError: " << TTF_GetError() << std::endl;
return creationFailed();
}
//Create the screen.
ScreenData screenData(createScreen());
if(!screenData) {
return creationFailed();
}
//Load key config. Then initialize joystick support.
inputMgr.loadConfig();
inputMgr.openAllJoysitcks();
//Init tinygettext for translations for the right language
dictionaryManager = new tinygettext::DictionaryManager();
dictionaryManager->set_use_fuzzy(false);
dictionaryManager->add_directory(getDataPath()+"locale");
dictionaryManager->set_charset("UTF-8");
//Disable annoying 'Couldn't translate: blah blah blah'
tinygettext::Log::set_log_info_callback(NULL);
//Check if user have defined own language. If not, find it out for the player using findlocale
string lang=getSettings()->getValue("lang");
if(lang.length()>0){
printf("Locale set by user to %s\n",lang.c_str());
language=lang;
}else{
FL_Locale *locale;
FL_FindLocale(&locale,FL_MESSAGES);
printf("Locale isn't set by user: %s\n",locale->lang);
language=locale->lang;
if(locale->country!=NULL){
language+=string("_")+string(locale->country);
}
if(locale->variant!=NULL){
language+=string("@")+string(locale->variant);
}
FL_FreeLocale(&locale);
}
//Now set the language in the dictionaryManager.
dictionaryManager->set_language(tinygettext::Language::from_name(language));
#ifdef WIN32
//Some ad-hoc fix for Windows since it accepts "zh-CN" but not "zh_CN"
std::string language2;
for (auto c : language) {
if (isalnum(c)) language2.push_back(c);
else if (c == '_') language2.push_back('-');
else break;
}
const char* languagePtr = language2.c_str();
#else
const char* languagePtr = language.c_str();
#endif
//Set time format.
setlocale(LC_TIME, languagePtr);
//Also set the numeric format for tinyformat.
tfm::setNumericFormat(
/// TRANSLATORS: This is the decimal point character in your language.
pgettext("numeric", "."),
/// TRANSLATORS: This is the thousands separator character in your language.
pgettext("numeric", ","),
/// TRANSLATORS: This is the grouping of digits in your language,
/// see <http://www.cplusplus.com/reference/locale/numpunct/grouping/> for more information.
/// However, we use string containing "123..." instead of "\x01\x02\x03...", also, "0" is the same as "".
pgettext("numeric", "3")
);
//Create the types of blocks.
for(int i=0;i<TYPE_MAX;i++){
Game::blockNameMap[Game::blockName[i]]=i;
}
//Structure that holds the event type/name pair.
struct EventTypeName{
int type;
const char* name;
};
//Create the types of game object event types.
{
const EventTypeName types[]={
{GameObjectEvent_PlayerWalkOn,"playerWalkOn"},
{GameObjectEvent_PlayerIsOn,"playerIsOn"},
{GameObjectEvent_PlayerLeave,"playerLeave"},
{GameObjectEvent_OnCreate,"onCreate"},
{GameObjectEvent_OnEnterFrame,"onEnterFrame"},
{ GameObjectEvent_OnPlayerInteraction, "onPlayerInteraction" },
{GameObjectEvent_OnToggle,"onToggle"},
{GameObjectEvent_OnSwitchOn,"onSwitchOn"},
{GameObjectEvent_OnSwitchOff,"onSwitchOff"},
{0,NULL}
};
for(int i=0;types[i].name;i++){
Game::gameObjectEventNameMap[types[i].name]=types[i].type;
Game::gameObjectEventTypeMap[types[i].type]=types[i].name;
}
}
//Create the types of level event types.
{
const EventTypeName types[]={
{LevelEvent_OnCreate,"onCreate"},
{LevelEvent_OnSave,"onSave"},
{LevelEvent_OnLoad,"onLoad"},
{0,NULL}
};
for(int i=0;types[i].name;i++){
Game::levelEventNameMap[types[i].name]=types[i].type;
Game::levelEventTypeMap[types[i].type]=types[i].name;
}
}
//Nothing went wrong so we return true.
return screenData;
}
bool loadFonts(){
//Load the fonts.
//NOTE: This is a separate method because it will be called separately when re-initing in case of language change.
//NOTE2: Since the font fallback is implemented, the font will not be loaded again if call loadFonts() twice.
if (fontMgr) {
return true;
}
fontMgr = new FontManager;
fontMgr->loadFonts();
fontTitle = fontMgr->getFont("fontTitle");
fontGUI = fontMgr->getFont("fontGUI");
fontGUISmall = fontMgr->getFont("fontGUISmall");
fontText = fontMgr->getFont("fontText");
fontMono = fontMgr->getFont("fontMono");
if (fontTitle == NULL || fontGUI == NULL || fontGUISmall == NULL || fontText == NULL || fontMono == NULL){
printf("FATAL ERROR: Unable to load fonts!\n");
return false;
}
//Nothing went wrong so return true.
return true;
}
//Generate small arrows used for some GUI widgets.
static void generateArrows(SDL_Renderer& renderer){
TTF_Font* fontArrow = fontMgr->getFont("fontArrow");
arrowLeft1=textureFromText(renderer,*fontArrow,"<",objThemes.getTextColor(false));
arrowRight1=textureFromText(renderer,*fontArrow,">",objThemes.getTextColor(false));
arrowLeft2=textureFromText(renderer,*fontArrow,"<",objThemes.getTextColor(true));
arrowRight2=textureFromText(renderer,*fontArrow,">",objThemes.getTextColor(true));
}
bool loadTheme(ImageManager& imageManager,SDL_Renderer& renderer,std::string name){
//Load default fallback theme if it isn't loaded yet
if(objThemes.themeCount()==0){
if(objThemes.appendThemeFromFile(getDataPath()+"themes/Cloudscape/theme.mnmstheme", imageManager, renderer)==NULL){
printf("ERROR: Can't load default theme file\n");
return false;
}
}
//Resize background or load specific theme
bool success=true;
if(name==""||name.empty()){
objThemes.scaleToScreen();
}else{
string theme=processFileName(name);
if(objThemes.appendThemeFromFile(theme+"/theme.mnmstheme", imageManager, renderer)==NULL){
printf("ERROR: Can't load theme %s\n",theme.c_str());
success=false;
}
}
generateArrows(renderer);
//Everything went fine so return true.
return success;
}
static SDL_Cursor* loadCursor(const char* image[]){
int i,row,col;
//The array that holds the data (0=white 1=black)
Uint8 data[4*32];
//The array that holds the alpha mask (0=transparent 1=visible)
Uint8 mask[4*32];
//The coordinates of the hotspot of the cursor.
int hotspotX, hotspotY;
i=-1;
//Loop through the rows and columns.
//NOTE: We assume a cursor size of 32x32.
for(row=0;row<32;++row){
for(col=0; col<32;++col){
if(col % 8) {
data[i]<<=1;
mask[i]<<=1;
}else{
++i;
data[i]=mask[i]=0;
}
switch(image[4+row][col]){
case '+':
data[i] |= 0x01;
mask[i] |= 0x01;
break;
case '.':
mask[i] |= 0x01;
break;
default:
break;
}
}
}
//Get the hotspot x and y locations from the last line of the cursor.
sscanf(image[4+row],"%d,%d",&hotspotX,&hotspotY);
return SDL_CreateCursor(data,mask,32,32,hotspotX,hotspotY);
}
bool loadFiles(ImageManager& imageManager, SDL_Renderer& renderer){
//Load the fonts.
if(!loadFonts())
return false;
//Show a loading screen
{
int w = 0,h = 0;
SDL_GetRendererOutputSize(&renderer, &w, &h);
SDL_Color fg={255,255,255,0};
TexturePtr loadingTexture = titleTextureFromText(renderer, _("Loading..."), fg, w);
SDL_Rect loadingRect = rectFromTexture(*loadingTexture);
loadingRect.x = (w-loadingRect.w)/2;
loadingRect.y = (h-loadingRect.h)/2;
SDL_RenderCopy(sdlRenderer, loadingTexture.get(), NULL, &loadingRect);
SDL_RenderPresent(sdlRenderer);
SDL_RenderClear(sdlRenderer);
}
musicManager.destroy();
//Load the music and play it.
if(musicManager.loadMusic((getDataPath()+"music/menu.music")).empty()){
printf("WARNING: Unable to load background music! \n");
}
musicManager.playMusic("menu",false);
//Load all the music lists from the data and user data path.
{
vector<string> musicLists=enumAllFiles((getDataPath()+"music/"),"list",true);
for(unsigned int i=0;i<musicLists.size();i++)
getMusicManager()->loadMusicList(musicLists[i]);
musicLists=enumAllFiles((getUserPath(USER_DATA)+"music/"),"list",true);
for(unsigned int i=0;i<musicLists.size();i++)
getMusicManager()->loadMusicList(musicLists[i]);
}
//Set the list to the configured one.
getMusicManager()->setMusicList(getSettings()->getValue("musiclist"));
//Check if music is enabled.
if(getSettings()->getBoolValue("music"))
getMusicManager()->setEnabled();
//Load the sound effects
soundManager.loadSound((getDataPath()+"sfx/jump.wav").c_str(),"jump");
soundManager.loadSound((getDataPath()+"sfx/hit.wav").c_str(),"hit");
soundManager.loadSound((getDataPath()+"sfx/checkpoint.wav").c_str(),"checkpoint");
soundManager.loadSound((getDataPath()+"sfx/swap.wav").c_str(),"swap");
soundManager.loadSound((getDataPath()+"sfx/toggle.ogg").c_str(),"toggle");
soundManager.loadSound((getDataPath()+"sfx/error.wav").c_str(),"error");
soundManager.loadSound((getDataPath()+"sfx/collect.wav").c_str(),"collect");
soundManager.loadSound((getDataPath()+"sfx/achievement.ogg").c_str(),"achievement");
//Load the cursor images from the Cursor.h file.
cursors[CURSOR_POINTER]=loadCursor(pointer);
cursors[CURSOR_CARROT]=loadCursor(ibeam);
cursors[CURSOR_DRAG]=loadCursor(closedhand);
cursors[CURSOR_SIZE_HOR]=loadCursor(size_hor);
cursors[CURSOR_SIZE_VER]=loadCursor(size_ver);
cursors[CURSOR_SIZE_FDIAG]=loadCursor(size_fdiag);
cursors[CURSOR_SIZE_BDIAG]=loadCursor(size_bdiag);
cursors[CURSOR_REMOVE]=loadCursor(remove_cursor);
cursors[CURSOR_POINTING_HAND] = loadCursor(pointing_hand);
//Set the default cursor right now.
SDL_SetCursor(cursors[CURSOR_POINTER]);
levelPackManager.destroy();
//Now sum up all the levelpacks.
vector<string> v=enumAllDirs(getDataPath()+"levelpacks/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelPackManager.loadLevelPack(getDataPath()+"levelpacks/"+*i);
}
v=enumAllDirs(getUserPath(USER_DATA)+"levelpacks/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelPackManager.loadLevelPack(getUserPath(USER_DATA)+"levelpacks/"+*i);
}
v=enumAllDirs(getUserPath(USER_DATA)+"custom/levelpacks/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelPackManager.loadLevelPack(getUserPath(USER_DATA)+"custom/levelpacks/"+*i);
}
//Now we add a special levelpack that will contain the levels not in a levelpack.
LevelPack* levelsPack=new LevelPack;
levelsPack->levelpackName="Levels";
levelsPack->levelpackPath=LEVELS_PATH;
levelsPack->type=COLLECTION;
LevelPack* customLevelsPack=new LevelPack;
customLevelsPack->levelpackName="Custom Levels";
customLevelsPack->levelpackPath=CUSTOM_LEVELS_PATH;
customLevelsPack->type=COLLECTION;
//List the main levels and add them one for one.
v = enumAllFiles(getDataPath() + "levels/");
for (vector<string>::iterator i = v.begin(); i != v.end(); ++i){
levelsPack->addLevel(getDataPath() + "levels/" + *i);
levelsPack->setLocked(levelsPack->getLevelCount() - 1);
}
//List the addon levels and add them one for one.
v=enumAllFiles(getUserPath(USER_DATA)+"levels/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelsPack->addLevel(getUserPath(USER_DATA)+"levels/"+*i);
levelsPack->setLocked(levelsPack->getLevelCount()-1);
}
//List the custom levels and add them one for one.
v=enumAllFiles(getUserPath(USER_DATA)+"custom/levels/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelsPack->addLevel(getUserPath(USER_DATA)+"custom/levels/"+*i);
levelsPack->setLocked(levelsPack->getLevelCount()-1);
customLevelsPack->addLevel(getUserPath(USER_DATA)+"custom/levels/"+*i);
customLevelsPack->setLocked(customLevelsPack->getLevelCount()-1);
}
//Add them to the manager.
levelPackManager.addLevelPack(levelsPack);
levelPackManager.addLevelPack(customLevelsPack);
//Load statistics
statsMgr.loadPicture(renderer, imageManager);
statsMgr.registerAchievements(imageManager);
statsMgr.loadFile(getUserPath(USER_CONFIG)+"statistics");
//Do something ugly and slow
statsMgr.reloadCompletedLevelsAndAchievements();
statsMgr.reloadOtherAchievements();
//Load the theme, both menu and default.
//NOTE: Loading theme may fail and returning false would stop everything, default theme will be used instead.
if (!loadTheme(imageManager,renderer,getSettings()->getValue("theme"))){
getSettings()->setValue("theme","%DATA%/themes/Cloudscape");
saveSettings();
}
//Nothing failed so return true.
return true;
}
bool loadSettings(){
//Check the version of config file.
int version = 0;
std::string cfgV05 = getUserPath(USER_CONFIG) + "meandmyshadow_V0.5.cfg";
std::string cfgV04 = getUserPath(USER_CONFIG) + "meandmyshadow.cfg";
if (fileExists(cfgV05.c_str())) {
//We find a config file of current version.
version = 0x000500;
} else if (fileExists(cfgV04.c_str())) {
//We find a config file of V0.4 version or earlier.
copyFile(cfgV04.c_str(), cfgV05.c_str());
version = 0x000400;
} else {
//No config file found, just create a new one.
version = 0x000500;
}
settings=new Settings(cfgV05);
settings->parseFile(version);
//Now apply settings changed through command line arguments, if any.
map<string,string>::iterator it;
for(it=tmpSettings.begin();it!=tmpSettings.end();++it){
settings->setValue(it->first,it->second);
}
tmpSettings.clear();
//Always return true?
return true;
}
bool saveSettings(){
return settings->save();
}
Settings* getSettings(){
return settings;
}
MusicManager* getMusicManager(){
return &musicManager;
}
SoundManager* getSoundManager(){
return &soundManager;
}
LevelPackManager* getLevelPackManager(){
return &levelPackManager;
}
void flipScreen(SDL_Renderer& renderer){
// Render the data from the back buffer.
SDL_RenderPresent(&renderer);
}
void clean(){
//Save statistics
statsMgr.saveFile(getUserPath(USER_CONFIG)+"statistics");
//We delete the settings.
if(settings){
delete settings;
settings=NULL;
}
//Delete dictionaryManager.
delete dictionaryManager;
//Get rid of the currentstate.
//NOTE: The state is probably already deleted by the changeState function.
if(currentState)
delete currentState;
//Destroy the GUI if present.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//These calls to destroy makes sure stuff is
//deleted before SDL is uninitialised (as these managers are stack allocated
//globals.)
//Destroy the musicManager.
musicManager.destroy();
//Destroy all sounds
soundManager.destroy();
//Destroy the cursors.
for(int i=0;i<CURSOR_MAX;i++){
SDL_FreeCursor(cursors[i]);
cursors[i]=NULL;
}
//Destroy the levelPackManager.
levelPackManager.destroy();
levels=NULL;
//Close all joysticks.
inputMgr.closeAllJoysticks();
//Close the fonts and quit SDL_ttf.
delete fontMgr;
fontMgr = NULL;
TTF_Quit();
//Remove the temp surface.
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow(sdlWindow);
arrowLeft1.reset(nullptr);
arrowLeft2.reset(nullptr);
arrowRight1.reset(nullptr);
arrowRight2.reset(nullptr);
//Stop audio.and quit
Mix_CloseAudio();
//SDL2 porting note. Not sure why this was only done on apple.
//#ifndef __APPLE__
Mix_Quit();
//#endif
//And finally quit SDL.
SDL_Quit();
}
void setNextState(int newstate){
//Only change the state when we aren't already exiting.
if(nextState!=STATE_EXIT){
nextState=newstate;
}
}
void changeState(ImageManager& imageManager, SDL_Renderer& renderer, int fade){
//Check if there's a nextState.
if(nextState!=STATE_NULL){
//Fade out, if fading is enabled.
if (currentState && settings->getBoolValue("fading")) {
for (; fade >= 0; fade -= 17) {
currentState->render(imageManager, renderer);
//TODO: Shouldn't the gamestate take care of rendering the GUI?
if (GUIObjectRoot) GUIObjectRoot->render(renderer);
dimScreen(renderer, static_cast<Uint8>(255 - fade));
//draw new achievements (if any) as overlay
statsMgr.render(imageManager, renderer);
flipScreen(renderer);
SDL_Delay(1000/FPS);
}
}
//Delete the currentState.
delete currentState;
currentState=NULL;
//Set the currentState to the nextState.
stateID=nextState;
nextState=STATE_NULL;
//Init the state.
switch(stateID){
case STATE_GAME:
{
currentState=NULL;
Game* game=new Game(renderer, imageManager);
currentState=game;
//Check if we should load record file or a level.
if(!Game::recordFile.empty()){
if (Game::recordFile[0] == '?') {
//This means load record file with current version of level.
game->loadRecord(imageManager, renderer, Game::recordFile.c_str() + 1, levels->getLevelFile().c_str());
} else {
game->loadRecord(imageManager, renderer, Game::recordFile.c_str());
}
Game::recordFile.clear();
}else{
game->loadLevel(imageManager,renderer,levels->getLevelFile());
levels->saveLevelProgress();
}
}
break;
case STATE_MENU:
currentState=new Menu(imageManager, renderer);
break;
case STATE_LEVEL_SELECT:
currentState=new LevelPlaySelect(imageManager, renderer);
break;
case STATE_LEVEL_EDIT_SELECT:
currentState=new LevelEditSelect(imageManager, renderer);
break;
case STATE_LEVEL_EDITOR:
{
currentState=NULL;
LevelEditor* levelEditor=new LevelEditor(renderer, imageManager);
currentState=levelEditor;
//Load the selected level.
levelEditor->loadLevel(imageManager,renderer,levels->getLevelFile());
}
break;
case STATE_OPTIONS:
currentState=new Options(imageManager, renderer);
break;
case STATE_ADDONS:
currentState=new Addons(renderer, imageManager);
break;
case STATE_CREDITS:
currentState=new Credits(imageManager,renderer);
break;
case STATE_STATISTICS:
currentState=new StatisticsScreen(imageManager,renderer);
break;
}
//NOTE: STATE_EXIT isn't mentioned, meaning that currentState is null.
//This way the game loop will break and the program will exit.
}
}
void musicStoppedHook(){
//We just call the musicStopped method of the MusicManager.
musicManager.musicStopped();
}
void channelFinishedHook(int channel){
soundManager.channelFinished(channel);
}
bool checkCollision(const SDL_Rect& a,const SDL_Rect& b){
//Check if the left side of box a isn't past the right side of b.
if(a.x>=b.x+b.w){
return false;
}
//Check if the right side of box a isn't left of the left side of b.
if(a.x+a.w<=b.x){
return false;
}
//Check if the top side of box a isn't under the bottom side of b.
if(a.y>=b.y+b.h){
return false;
}
//Check if the bottom side of box a isn't above the top side of b.
if(a.y+a.h<=b.y){
return false;
}
//We have collision.
return true;
}
bool pointOnRect(const SDL_Rect& point, const SDL_Rect& rect) {
if(point.x >= rect.x && point.x < rect.x + rect.w
&& point.y >= rect.y && point.y < rect.y + rect.h) {
return true;
}
return false;
}
int parseArguments(int argc, char** argv){
//Loop through all arguments.
//We start at one since 0 is the command itself.
for(int i=1;i<argc;i++){
string argument=argv[i];
//Check if the argument is the data-dir.
if(argument=="--data-dir"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Configure the dataPath with the given path.
dataPath=argv[i];
if(!getDataPath().empty()){
char c=dataPath[dataPath.size()-1];
if(c!='/'&&c!='\\') dataPath+="/";
}
}else if(argument=="--user-dir"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Configure the userPath with the given path.
userPath=argv[i];
if(!userPath.empty()){
char c=userPath[userPath.size()-1];
if(c!='/'&&c!='\\') userPath+="/";
}
}else if(argument=="-f" || argument=="-fullscreen" || argument=="--fullscreen"){
tmpSettings["fullscreen"]="1";
}else if(argument=="-w" || argument=="-windowed" || argument=="--windowed"){
tmpSettings["fullscreen"]="0";
}else if(argument=="-mv" || argument=="-music" || argument=="--music"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Now set the music volume.
tmpSettings["music"]=argv[i];
}else if(argument=="-sv" || argument=="-sound" || argument=="--sound"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Now set sound volume.
tmpSettings["sound"]=argv[i];
}else if(argument=="-set" || argument=="--set"){
//We need a second and a third argument so we increase i.
i+=2;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//And set the setting.
tmpSettings[argv[i-1]]=argv[i];
}else if(argument=="-v" || argument=="-version" || argument=="--version"){
//Print the version.
printf("%s\n",version.c_str());
return 0;
}else if(argument=="-h" || argument=="-help" || argument=="--help"){
//If the help is requested we'll return false without printing an error.
//This way the usage/help text will be printed.
return -1;
}else{
//Any other argument is unknow so we return false.
printf("ERROR: Unknown argument %s\n\n",argument.c_str());
return -1;
}
}
//If everything went well we can return true.
return 1;
}
//Special structure that will recieve the GUIEventCallbacks of the messagebox.
struct msgBoxHandler:public GUIEventCallback{
public:
//Integer containing the ret(urn) value of the messageBox.
int ret;
public:
//Constructor.
msgBoxHandler():ret(0){}
void GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Make sure it's a click event.
if(eventType==GUIEventClick){
//Set the return value.
ret=obj->value;
//After a click event we can delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
}
};
msgBoxResult msgBox(ImageManager& imageManager,SDL_Renderer& renderer, const string& prompt,msgBoxButtons buttons,const string& title){
//Create the event handler.
msgBoxHandler objHandler;
//Create the GUIObjectRoot, the height and y location is temp.
//It depends on the content what it will be.
GUIObject* root=new GUIFrame(imageManager,renderer,(SCREEN_WIDTH-600)/2,200,600,200,title.c_str());
//Integer containing the current y location used to grow dynamic depending on the content.
int y=50;
//Now process the prompt.
{
//NOTE: We shouldn't modify the cotents in the c_str() of a string,
//since it's said that at least in g++ the std::string is copy-on-write
//hence if we modify the content it may break
//The copy of the prompt.
std::vector<char> copyOfPrompt(prompt.begin(), prompt.end());
//Append another '\0' to it.
copyOfPrompt.push_back(0);
//Pointer to the string.
char* lps = &(copyOfPrompt[0]);
//Pointer to a character.
char* lp=NULL;
//The list of labels.
std::vector<GUIObject*> labels;
//We keep looping forever.
//The only way out is with the break statement.
for(;;){
//As long as it's still the same sentence we continue.
//It will stop when there's a newline or end of line.
for(lp=lps;*lp!='\n'&&*lp!='\r'&&*lp!=0;lp++);
//Store the character we stopped on. (End or newline)
char c=*lp;
//Set the character in the string to 0, making lps a string containing one sentence.
*lp=0;
//Add a GUIObjectLabel with the sentence.
GUIObject *label = new GUILabel(imageManager, renderer, 0, y, root->width, 25, lps, 0, true, true, GUIGravityCenter);
labels.push_back(label);
root->addChild(label);
//Calculate the width of the text.
int w = 0;
TTF_SizeUTF8(fontText, lps, &w, NULL);
w += 20;
if (w > root->width) root->width = w;
//Increase y with 25, about the height of the text.
y+=25;
//Check the stored character if it was a stop.
if(c==0){
//It was so break out of the for loop.
lps=lp;
break;
}
//It wasn't meaning more will follow.
//We set lps to point after the "newline" forming a new string.
lps=lp+1;
}
//Shrink the dialog if it's too big.
if (root->width > SCREEN_WIDTH - 20) root->width = SCREEN_WIDTH - 20;
root->left = (SCREEN_WIDTH - root->width) / 2;
//Move labels to their correct locations.
for (auto label : labels) {
label->width = root->width;
}
}
//Add 70 to y to leave some space between the content and the buttons.
y+=70;
//Recalc the size of the message box.
root->top=(SCREEN_HEIGHT-y)/2;
root->height=y;
//Now we need to add the buttons.
//Integer containing the number of buttons to add.
int count=0;
//Array with the return codes for the buttons.
int value[3]={0};
//Array containing the captation for the buttons.
string button[3]={"","",""};
switch(buttons){
case MsgBoxOKCancel:
count=2;
button[0]=_("OK");value[0]=MsgBoxOK;
button[1]=_("Cancel");value[1]=MsgBoxCancel;
break;
case MsgBoxAbortRetryIgnore:
count=3;
button[0]=_("Abort");value[0]=MsgBoxAbort;
button[1]=_("Retry");value[1]=MsgBoxRetry;
button[2]=_("Ignore");value[2]=MsgBoxIgnore;
break;
case MsgBoxYesNoCancel:
count=3;
button[0]=_("Yes");value[0]=MsgBoxYes;
button[1]=_("No");value[1]=MsgBoxNo;
button[2]=_("Cancel");value[2]=MsgBoxCancel;
break;
case MsgBoxYesNo:
count=2;
button[0]=_("Yes");value[0]=MsgBoxYes;
button[1]=_("No");value[1]=MsgBoxNo;
break;
case MsgBoxRetryCancel:
count=2;
button[0]=_("Retry");value[0]=MsgBoxRetry;
button[1]=_("Cancel");value[1]=MsgBoxCancel;
break;
default:
count=1;
button[0]=_("OK");value[0]=MsgBoxOK;
break;
}
//Now we start making the buttons.
{
//Reduce y so that the buttons fit inside the frame.
y-=40;
double places[3]={0.0};
if(count==1){
places[0]=0.5;
}else if(count==2){
places[0]=0.35;
places[1]=0.65;
}else if(count==3){
places[0]=0.25;
places[1]=0.5;
places[2]=0.75;
}
std::vector<GUIButton*> buttons;
//Loop to add the buttons.
for(int i=0;i<count;i++){
GUIButton* obj = new GUIButton(imageManager, renderer, root->width*places[i], y, -1, 36, button[i].c_str(), value[i], true, true, GUIGravityCenter);
obj->eventCallback=&objHandler;
buttons.push_back(obj);
root->addChild(obj);
}
//Update widgets
for (int i = 0; i < count; i++) {
buttons[i]->render(renderer, 0, 0, false);
}
bool overlap = false;
//Check if they overlap
if (buttons[0]->left - buttons[0]->gravityX < 5 ||
buttons[count - 1]->left - buttons[count - 1]->gravityX + buttons[count - 1]->width > root->width - 5)
{
overlap = true;
} else {
for (int i = 0; i < count - 1; i++) {
if (buttons[i]->left - buttons[i]->gravityX + buttons[i]->width >= buttons[i + 1]->left - buttons[i + 1]->gravityX) {
overlap = true;
break;
}
}
}
//Shrink the font size if any buttons are overlap
if (overlap) {
for (int i = 0; i < count; i++) {
buttons[i]->smallFont = true;
buttons[i]->width = -1;
}
}
}
//Now we dim the screen and keep the GUI rendering/updating.
GUIOverlay* overlay=new GUIOverlay(renderer,root);
overlay->keyboardNavigationMode = LeftRightFocus | UpDownFocus | TabFocus | ((count == 1) ? 0 : ReturnControls);
overlay->enterLoop(imageManager, renderer, true, count == 1);
//And return the result.
return (msgBoxResult)objHandler.ret;
}
// A helper function to read a character from utf8 string
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadForward(const char* s, int& p) {
int ch = (unsigned char)s[p];
if (ch < 0x80){
if (ch) p++;
return ch;
} else if (ch < 0xC0){
// skip invalid characters
while (((unsigned char)s[p] & 0xC0) == 0x80) p++;
return -1;
} else if (ch < 0xE0){
int c2 = (unsigned char)s[++p];
if ((c2 & 0xC0) != 0x80) return -1;
ch = ((ch & 0x1F) << 6) | (c2 & 0x3F);
p++;
return ch;
} else if (ch < 0xF0){
int c2 = (unsigned char)s[++p];
if ((c2 & 0xC0) != 0x80) return -1;
int c3 = (unsigned char)s[++p];
if ((c3 & 0xC0) != 0x80) return -1;
ch = ((ch & 0xF) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
p++;
return ch;
} else if (ch < 0xF8){
int c2 = (unsigned char)s[++p];
if ((c2 & 0xC0) != 0x80) return -1;
int c3 = (unsigned char)s[++p];
if ((c3 & 0xC0) != 0x80) return -1;
int c4 = (unsigned char)s[++p];
if ((c4 & 0xC0) != 0x80) return -1;
ch = ((ch & 0x7) << 18) | ((c2 & 0x3F) << 12) | ((c3 & 0x3F) << 6) | (c4 & 0x3F);
if (ch >= 0x110000) ch = -1;
p++;
return ch;
} else {
p++;
return -1;
}
}
// A helper function to read a character backward from utf8 string (experimental)
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadBackward(const char* s, int& p) {
if (p <= 0) return 0;
do {
p--;
} while (p > 0 && ((unsigned char)s[p] & 0xC0) == 0x80);
int tmp = p;
return utf8ReadForward(s, tmp);
}
#ifndef WIN32
// ad-hoc function to check if a program is installed
static bool programExists(const std::string& program) {
std::string p = tfm::format("which \"%s\" 2>&1", program);
const int BUFSIZE = 128;
char buf[BUFSIZE];
FILE *fp;
if ((fp = popen(p.c_str(), "r")) == NULL) {
return false;
}
while (fgets(buf, BUFSIZE, fp) != NULL) {
// Drop all outputs since 'which' returns -1 when the program is not found
}
if (pclose(fp)) {
return false;
}
return true;
}
#endif
void openWebsite(const std::string& url) {
#ifdef WIN32
wchar_t ws[4096];
TO_UTF16(url.c_str(), ws);
SDL_SysWMinfo info = {};
SDL_VERSION(&info.version);
SDL_GetWindowWMInfo(sdlWindow, &info);
ShellExecuteW(info.info.win.window, L"open", ws, NULL, NULL, SW_SHOW);
#else
static int method = -1;
// Some of these methods are copied from https://stackoverflow.com/questions/5116473/
const char* methods[] = {
"xdg-open", "xdg-open \"%s\"",
"gnome-open", "gnome-open \"%s\"",
"kde-open", "kde-open \"%s\"",
"open", "open \"%s\"",
"python", "python -m webbrowser \"%s\"",
"sensible-browser", "sensible-browser \"%s\"",
"x-www-browser", "x-www-browser \"%s\"",
NULL,
};
if (method < 0) {
for (method = 0; methods[method]; method += 2) {
if (programExists(methods[method])) break;
}
}
if (methods[method]) {
std::string p = tfm::format(methods[method + 1], url);
system(p.c_str());
} else {
fprintf(stderr, "TODO: openWebsite is not implemented on your system\n");
}
#endif
}
std::string appendURLToLicense(const std::string& license) {
// if the license doesn't include url, try to detect it from a predefined list
if (license.find("://") == std::string::npos) {
std::string normalized;
for (char c : license) {
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')) {
normalized.push_back(c);
} else if (c >= 'a' && c <= 'z') {
normalized.push_back(c + ('A' - 'a'));
}
}
const char* licenses[] = {
// AGPL
"AGPL1", "AGPLV1", NULL, "http://www.affero.org/oagpl.html",
"AGPL2", "AGPLV2", NULL, "http://www.affero.org/agpl2.html",
"AGPL", NULL, "https://gnu.org/licenses/agpl.html",
// LGPL
"LGPL21", "LGPLV21", NULL, "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html",
"LGPL2", "LGPLV2", NULL, "https://www.gnu.org/licenses/old-licenses/lgpl-2.0.html",
"LGPL", NULL, "https://www.gnu.org/copyleft/lesser.html",
// GPL
"GPL1", "GPLV1", NULL, "https://www.gnu.org/licenses/old-licenses/gpl-1.0.html",
"GPL2", "GPLV2", NULL, "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html",
"GPL", NULL, "https://gnu.org/licenses/gpl.html",
// CC BY-NC-ND
"CCBYNCND1", "CCBYNDNC1", NULL, "https://creativecommons.org/licenses/by-nd-nc/1.0",
"CCBYNCND25", "CCBYNDNC25", NULL, "https://creativecommons.org/licenses/by-nc-nd/2.5",
"CCBYNCND2", "CCBYNDNC2", NULL, "https://creativecommons.org/licenses/by-nc-nd/2.0",
"CCBYNCND3", "CCBYNDNC3", NULL, "https://creativecommons.org/licenses/by-nc-nd/3.0",
"CCBYNCND", "CCBYNDNC", NULL, "https://creativecommons.org/licenses/by-nc-nd/4.0",
// CC BY-NC-SA
"CCBYNCSA1", NULL, "https://creativecommons.org/licenses/by-nc-sa/1.0",
"CCBYNCSA25", NULL, "https://creativecommons.org/licenses/by-nc-sa/2.5",
"CCBYNCSA2", NULL, "https://creativecommons.org/licenses/by-nc-sa/2.0",
"CCBYNCSA3", NULL, "https://creativecommons.org/licenses/by-nc-sa/3.0",
"CCBYNCSA", NULL, "https://creativecommons.org/licenses/by-nc-sa/4.0",
// CC BY-ND
"CCBYND1", NULL, "https://creativecommons.org/licenses/by-nd/1.0",
"CCBYND25", NULL, "https://creativecommons.org/licenses/by-nd/2.5",
"CCBYND2", NULL, "https://creativecommons.org/licenses/by-nd/2.0",
"CCBYND3", NULL, "https://creativecommons.org/licenses/by-nd/3.0",
"CCBYND", NULL, "https://creativecommons.org/licenses/by-nd/4.0",
// CC BY-NC
"CCBYNC1", NULL, "https://creativecommons.org/licenses/by-nc/1.0",
"CCBYNC25", NULL, "https://creativecommons.org/licenses/by-nc/2.5",
"CCBYNC2", NULL, "https://creativecommons.org/licenses/by-nc/2.0",
"CCBYNC3", NULL, "https://creativecommons.org/licenses/by-nc/3.0",
"CCBYNC", NULL, "https://creativecommons.org/licenses/by-nc/4.0",
// CC BY-SA
"CCBYSA1", NULL, "https://creativecommons.org/licenses/by-sa/1.0",
"CCBYSA25", NULL, "https://creativecommons.org/licenses/by-sa/2.5",
"CCBYSA2", NULL, "https://creativecommons.org/licenses/by-sa/2.0",
"CCBYSA3", NULL, "https://creativecommons.org/licenses/by-sa/3.0",
"CCBYSA", NULL, "https://creativecommons.org/licenses/by-sa/4.0",
// CC BY
"CCBY1", NULL, "https://creativecommons.org/licenses/by/1.0",
"CCBY25", NULL, "https://creativecommons.org/licenses/by/2.5",
"CCBY2", NULL, "https://creativecommons.org/licenses/by/2.0",
"CCBY3", NULL, "https://creativecommons.org/licenses/by/3.0",
"CCBY", NULL, "https://creativecommons.org/licenses/by/4.0",
// CC0
"CC0", NULL, "https://creativecommons.org/publicdomain/zero/1.0",
// WTFPL
"WTFPL", NULL, "http://www.wtfpl.net/",
// end
NULL,
};
for (int i = 0; licenses[i]; i++) {
bool found = false;
for (; licenses[i]; i++) {
if (normalized.find(licenses[i]) != std::string::npos) found = true;
}
i++;
if (found) {
return license + tfm::format(" <%s>", licenses[i]);
}
}
}
return license;
}
int getKeyboardRepeatDelay() {
static int ret = -1;
if (ret < 0) {
#ifdef WIN32
int i = 0;
SystemParametersInfoW(SPI_GETKEYBOARDDELAY, 0, &i, 0);
// NOTE: these weird numbers are derived from Microsoft's documentation explaining the return value of SystemParametersInfo.
i = clamp(i, 0, 3);
ret = (i + 1) * 10;
#else
// TODO: platform-dependent code
// Assume it's 250ms, i.e. 10 frames
ret = 10;
#endif
// Debug
#ifdef _DEBUG
printf("getKeyboardRepeatDelay() = %d\n", ret);
#endif
}
return ret;
}
int getKeyboardRepeatInterval() {
static int ret = -1;
if (ret < 0) {
#ifdef WIN32
int i = 0;
SystemParametersInfoW(SPI_GETKEYBOARDSPEED, 0, &i, 0);
// NOTE: these weird numbers are derived from Microsoft's documentation explaining the return value of SystemParametersInfo.
i = clamp(i, 0, 31);
ret = (int)floor(40.0f / (2.5f + 0.887097f * (float)i) + 0.5f);
#else
// TODO: platform-dependent code
// Assume it's 25ms, i.e. 1 frame
ret = 1;
#endif
// Debug
#ifdef _DEBUG
printf("getKeyboardRepeatInterval() = %d\n", ret);
#endif
}
return ret;
}
+
+std::string escapeNewline(const std::string& src) {
+ std::string ret;
+ for (auto c : src) {
+ switch (c) {
+ case '\r':
+ break;
+ case '\n':
+ ret += "\\n";
+ break;
+ case '\\':
+ ret += "\\\\";
+ break;
+ default:
+ ret.push_back(c);
+ break;
+ }
+ }
+ return ret;
+}
+
+std::string unescapeNewline(const std::string& src) {
+ std::string ret;
+ for (int i = 0, m = src.size(); i < m; i++) {
+ char c = src[i];
+ if (c == '\\' && i + 1 < m) {
+ i++;
+ char c2 = src[i];
+ switch (c2) {
+ case 'r':
+ break;
+ case 'n':
+ ret.push_back('\n');
+ break;
+ case '\\':
+ ret.push_back('\\');
+ break;
+ default:
+ ret.push_back(c);
+ ret.push_back(c2);
+ break;
+ }
+ } else {
+ ret.push_back(c);
+ }
+ }
+ return ret;
+}
diff --git a/src/Functions.h b/src/Functions.h
index 2319404..727e889 100644
--- a/src/Functions.h
+++ b/src/Functions.h
@@ -1,300 +1,306 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#include "Settings.h"
#include "Globals.h"
#include <SDL.h>
#include <string>
#include <vector>
class MusicManager;
class SoundManager;
//gettext function
//message: The message to translate.
//Returns: The translated string or the original string if there is not translation available.
#define _(message) (dictionaryManager!=NULL?dictionaryManager->get_dictionary().translate(message).c_str():std::string(message).c_str())
//gettext function
//NOTE: "_C" is conflict to some Android macros so we change its name.
//dictionaryManager: Pointer to the dictionaryManager to use for the translation.
//message: The message to translate.
//Returns: The translated string or the original string if there is not translation available.
#define _CC(dictionaryManager, message) ((dictionaryManager)!=NULL?(dictionaryManager)->get_dictionary().translate(message).c_str():std::string(message).c_str())
//dummy function for xgettext
//message: The message to translate.
//Returns: message parameter
#define __(message) (message)
class ImageManager;
struct SDL_Texture;
class LevelPackManager;
//gettext function for contexts
//context: The context of the message.
//message: The message to translate.
//Returns: The translated string or the original string if there are no translations available.
std::string pgettext(const std::string& context, const std::string& message);
//gettext function for plural forms
//message: The singular version of the message to translate.
//messageplural: The plural version of the message to translate.
//num: The number to fetch the plural form for
//Returns: The translated string or the original string if there are no translations available.
std::string ngettext(const std::string& message, const std::string& messageplural, int num);
//Method for drawing an SDL_Surface onto another.
//x: The x location to draw the source on the desination.
//y: The y location to draw the source on the desination.
//source: The SDL_Surface to draw.
//dest: The SDL_Surface to draw on.
//clip: Rectangle which part of the source should be drawn.
void applySurface(int x,int y,SDL_Surface* source,SDL_Surface* dest,SDL_Rect* clip);
//Method used to draw an rectangle.
//x: The top left x location of the rectangle.
//y: The top left y location of the rectangle.
//w: The width of the rectangle,
//h: The height of the rectangle.
//dest: The SDL_Surface to draw on.
//color: The color of the rectangle border to draw.
void drawRect(int x, int y, int w, int h, SDL_Renderer &renderer, Uint32 color=0);
//Method used to draw filled boxes with an anti-alliased border.
//Mostly used for GUI components.
//x: The top left x location of the box.
//y: The top left y location of the box.
//w: The width of the box,
//h: The height of the box.
//renderer: The SDL_Renderer to render on..
//color: The color of the rectangle background to draw.
void drawGUIBox(int x,int y,int w,int h,SDL_Renderer& renderer,Uint32 color);
//Method used to draw a line.
//x1: The x location of the start point.
//y1: The y location of the start point.
//x2: The x location of the end point.
//y2: The y location of the end point.
//dest: The SDL_Surface to draw on.
//color: The color of the line to draw.
void drawLine(int x1,int y1,int x2,int y2,SDL_Renderer &renderer,Uint32 color=0);
//Method used to draw a line with some arrows on it.
//x1: The x location of the start point.
//y1: The y location of the start point.
//x2: The x location of the end point.
//y2: The y location of the end point.
//dest: The SDL_Surface to draw on.
//color: The color of the line to draw.
//spacing: The spacing between arrows.
//offset: Offset of first arrow relative to the start point.
//xize, ysize: The size of arrow.
void drawLineWithArrow(int x1, int y1, int x2, int y2, SDL_Renderer &renderer, Uint32 color=0, int spacing=16, int offset=0, int xsize=5, int ysize=5);
//Method that will load the fonts needed for the game.
//NOTE: It's separate from loadFiles(), since it might get called separatly from the code when changing the language.
bool loadFonts();
//Method that will load the default theme again.
//name: name of the theme to load or empty for scaling background
//NOTE: It's separate from loadFiles(), since it might get called separatly from the code when changing resolution.
bool loadTheme(ImageManager& imageManager, SDL_Renderer& renderer, std::string name);
struct ScreenData{
SDL_Renderer* renderer;
explicit operator bool() const {
return renderer!=nullptr;
}
};
//This method will attempt to create the screen/window.
//NOTE: It's separate from init(), since it might get called separatly from the code when changing resolution.
ScreenData createScreen();
//Method for retrieving a list of resolutions.
std::vector<SDL_Point> getResolutionList();
//Method that is called when a fullscreen window is created.
//It will choose the resolution that is closest to the configured one.
void pickFullscreenResolution();
//This method is used to configure the window that is created by createScreen.
//NOTE: It will do it in a WM specific way, so if the wm is unkown it will do nothing.
void configureWindow();
//Call this method when receive SDL_VIDEORESIZE event.
void onVideoResize(ImageManager &imageManager, SDL_Renderer& renderer);
//Initialises the game. This is done almost at the beginning of the program.
//It initialises: SDL, SDL_Mixer, SDL_ttf, the screen and the block types.
//Returns: True if everything goes well.
ScreenData init();
//Loads some important files, like the background music and the default theme.
//Returns: True if everything goes well.
bool loadFiles(ImageManager &imageManager, SDL_Renderer &renderer);
//This method will load the settings from the settings file.
//Returns: False if there's an error while loading.
bool loadSettings();
//This method will save the settings to the settings file.
//Returns: False if there's an error while saving.
bool saveSettings();
//Method used to get a pointer to the settings object.
//Returns: A pointer to the settings object.
Settings* getSettings();
//Method used to get a pointer to the MusicManager object.
//Returns: A pointer to the MusicManager object.
MusicManager* getMusicManager();
//Method used to get a pointer to the SoundManager object.
//Returns: A pointer to the SoundManager object.
SoundManager* getSoundManager();
//Method used to get a pointer to the LevelPackManager object.
//Returns: A pointer to the LevelPackManager object.
LevelPackManager* getLevelPackManager();
//Method that will, depending on the rendering backend, draw the screen surface to the screen.
void flipScreen(SDL_Renderer& renderer);
//Method used to clean up before quiting meandmyshadow.
void clean();
//Sets what the nextState will be.
//newstate: Integer containing the id of the newstate.
void setNextState(int newstate);
//Method that will perform the state change.
//It will fade out (but not fade in).
void changeState(ImageManager &imageManager, SDL_Renderer &renderer, int fade = 255);
//This method is called when music is stopped.
//NOTE: This method is outside the MusicManager because it can't be called otherwise by SDL_Mixer.
//Do not call this method anywhere in the code!
void musicStoppedHook();
//This method is called when a sfx finished playing.
//NOTE: This method is outside the SoundManager because it can't be called otherwise by SDL_Mixer.
//Do not call this method anywhere in the code!
//channel: The number of the channel that is finished.
void channelFinishedHook(int channel);
//Checks collision between two SDL_Rects.
//a: The first rectangle.
//b: The second rectangle.
//Returns: True if the two rectangles collide.
bool checkCollision(const SDL_Rect& a,const SDL_Rect& b);
//Checks if a given point lays on an SDL_Rect
//point: The point to check.
//rect: The rectangle to check.
//Returns: True if the point is on the rectangle.
bool pointOnRect(const SDL_Rect& a,const SDL_Rect& b);
//Parse the commandline arguments.
//argc: Integer containing the number of aruguments there are.
//argv: The arguments.
//Returns: -1 if something goes wrong while parsing,
// 0 if version is shown,
// 1 if everything is alright
int parseArguments(int argc, char** argv);
//From http://en.wikipedia.org/wiki/Clamping_(graphics)
//x: The value to clamp.
//min: The minimum x can be.
//max: The maximum x can be.
//Returns: Integer containing the clamped value.
int inline clamp(int x,int min,int max){
return (x>max)?max:(x<min)?min:x;
}
//Enumeration containing the different messagebox button combinations.
enum msgBoxButtons{
//Only one button with the text OK.
MsgBoxOKOnly=0,
//Two buttons, one saying OK, the other Cancel.
MsgBoxOKCancel=1,
//Three buttons, Abort, Retry, Ignore.
MsgBoxAbortRetryIgnore=2,
//Three buttons, Yes, No or Cancel.
MsgBoxYesNoCancel=3,
//Two buttons, one saying Yes, the other No.
MsgBoxYesNo=4,
//Two buttons, one saying Retry, the other Cancel.
MsgBoxRetryCancel=5,
};
//Enumeration containing the different result that can be retrieved from a messagebox.
//It represents the button that has been pressed.
enum msgBoxResult{
//The OK button.
MsgBoxOK=1,
//The cancel button.
MsgBoxCancel=2,
//The abort button.
MsgBoxAbort=3,
//The retry button.
MsgBoxRetry=4,
//The ignore button.
MsgBoxIgnore=5,
//The yes button.
MsgBoxYes=6,
//The no button.
MsgBoxNo=7,
};
//Method that prompts the user with a notification and/or question.
//prompt: The message the user is prompted with.
//buttons: Which buttons the messagebox should have.
//title: The title of the message box.
//Returns: A msgBoxResult which button has been pressed.
msgBoxResult msgBox(ImageManager& imageManager,SDL_Renderer& renderer, const std::string& prompt,msgBoxButtons buttons,const std::string& title);
// A helper function to read a character from utf8 string
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadForward(const char* s, int& p);
// A helper function to read a character backward from utf8 string (experimental)
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadBackward(const char* s, int& p);
// Open the website using default web browser.
// url: The url of the website. Currently only http and https are supported.
// Also we assume that the url only contains ASCII characters.
// WARNING: Passing other url may result in arbitrary behavior (esp. passing '*.exe' on Windows).
void openWebsite(const std::string& url);
// Append a URL to a license if the license doesn't include URL (try to detect it from a predefined list).
// license: The license.
// Return value: The license appended with a URL if we detect the license successfully.
std::string appendURLToLicense(const std::string& license);
// Retrieves the (approximate) keyboard repeat delay time, in frames (NOTE: frame rate is hardcoded as 40).
int getKeyboardRepeatDelay();
// Retrieves the (approximate) keyboard repeat interval time, in frames (NOTE: frame rate is hardcoded as 40).
int getKeyboardRepeatInterval();
+// Escape the newline '\n' and '\\', and removes '\r'.
+std::string escapeNewline(const std::string& src);
+
+// Unescape the newline '\n' and '\\'.
+std::string unescapeNewline(const std::string& src);
+
#endif
diff --git a/src/Game.cpp b/src/Game.cpp
index 47a1013..9c7ece5 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -1,2127 +1,2129 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Block.h"
#include "GameState.h"
#include "Functions.h"
#include "GameObjects.h"
#include "ThemeManager.h"
#include "Game.h"
#include "LevelEditor.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "InputManager.h"
#include "MusicManager.h"
#include "Render.h"
#include "StatisticsManager.h"
#include "ScriptExecutor.h"
#include "MD5.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include <map>
#include <algorithm>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <SDL_ttf.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
const char* Game::blockName[TYPE_MAX]={"Block","PlayerStart","ShadowStart",
"Exit","ShadowBlock","Spikes",
"Checkpoint","Swap","Fragile",
"MovingBlock","MovingShadowBlock","MovingSpikes",
"Teleporter","Button","Switch",
"ConveyorBelt","ShadowConveyorBelt","NotificationBlock", "Collectable", "Pushable"
};
map<string,int> Game::blockNameMap;
map<int,string> Game::gameObjectEventTypeMap;
map<string,int> Game::gameObjectEventNameMap;
map<int,string> Game::levelEventTypeMap;
map<string,int> Game::levelEventNameMap;
string Game::recordFile;
//An internal function.
static void copyCompiledScripts(lua_State *state, const std::map<int, int>& src, std::map<int, int>& dest) {
//Clear the existing scripts.
for (auto it = dest.begin(); it != dest.end(); ++it) {
luaL_unref(state, LUA_REGISTRYINDEX, it->second);
}
dest.clear();
//Copy the source to the destination.
for (auto it = src.begin(); it != src.end(); ++it) {
lua_rawgeti(state, LUA_REGISTRYINDEX, it->second);
dest[it->first] = luaL_ref(state, LUA_REGISTRYINDEX);
}
}
//An internal function.
static void copyLevelObjects(const std::vector<Block*>& src, std::vector<Block*>& dest, bool setActive) {
//Clear the existing objects.
for (auto o : dest) delete o;
dest.clear();
//Copy the source to the destination.
for (auto o : src) {
if (o == NULL || o->isDelete) continue;
Block* o2 = new Block(*o);
dest.push_back(o2);
if (setActive) o2->setActive();
}
}
Game::Game(SDL_Renderer &renderer, ImageManager &imageManager):isReset(false)
, scriptExecutor(new ScriptExecutor())
,currentLevelNode(NULL)
,customTheme(NULL)
,background(NULL)
, levelRect(SDL_Rect{ 0, 0, 0, 0 }), levelRectSaved(SDL_Rect{ 0, 0, 0, 0 }), levelRectInitial(SDL_Rect{ 0, 0, 0, 0 })
, arcade(false)
,won(false)
,interlevel(false)
,time(0),timeSaved(0)
,recordings(0),recordingsSaved(0)
,cameraMode(CAMERA_PLAYER),cameraModeSaved(CAMERA_PLAYER)
,player(this),shadow(this),objLastCheckPoint(NULL)
, currentCollectables(0), currentCollectablesSaved(0), currentCollectablesInitial(0)
, totalCollectables(0), totalCollectablesSaved(0), totalCollectablesInitial(0)
{
saveStateNextTime=false;
loadStateNextTime=false;
recentSwap=recentSwapSaved=-10000;
recentLoad=recentSave=0;
action=imageManager.loadTexture(getDataPath()+"gfx/actions.png", renderer);
medals=imageManager.loadTexture(getDataPath()+"gfx/medals.png", renderer);
//Get the collectable image from the theme.
//NOTE: Isn't there a better way to retrieve the image?
objThemes.getBlock(TYPE_COLLECTABLE)->createInstance(&collectable);
//Hide the cursor if not in the leveleditor.
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
Game::~Game(){
//Simply call our destroy method.
destroy();
//Before we leave make sure the cursor is visible.
SDL_ShowCursor(SDL_ENABLE);
}
void Game::destroy(){
delete scriptExecutor;
scriptExecutor = NULL;
//Loop through the levelObjects, etc. and delete them.
for (auto o : levelObjects) delete o;
levelObjects.clear();
for (auto o : levelObjectsSave) delete o;
levelObjectsSave.clear();
for (auto o : levelObjectsInitial) delete o;
levelObjectsInitial.clear();
//Loop through the sceneryLayers and delete them.
for(auto it=sceneryLayers.begin();it!=sceneryLayers.end();++it){
delete it->second;
}
sceneryLayers.clear();
//Clear the name and the editor data.
levelName.clear();
levelFile.clear();
editorData.clear();
//Remove everything from the themeManager.
background=NULL;
if(customTheme)
objThemes.removeTheme();
customTheme=NULL;
//If there's a (partial) theme bundled with the levelpack remove that as well.
if(levels->customTheme)
objThemes.removeTheme();
//delete current level (if any)
if(currentLevelNode){
delete currentLevelNode;
currentLevelNode=NULL;
}
//Reset the time.
time=timeSaved=0;
recordings=recordingsSaved=0;
recentSwap=recentSwapSaved=-10000;
//Set the music list back to the configured list.
getMusicManager()->setMusicList(getSettings()->getValue("musiclist"));
}
void Game::reloadMusic() {
//NOTE: level music is always enabled.
//Check if the levelpack has a prefered music list.
if (levels && !levels->levelpackMusicList.empty())
getMusicManager()->setMusicList(levels->levelpackMusicList);
//Check for the music to use.
string &s = editorData["music"];
if (!s.empty()) {
getMusicManager()->playMusic(s);
} else {
getMusicManager()->pickMusic();
}
}
void Game::loadLevelFromNode(ImageManager& imageManager,SDL_Renderer& renderer,TreeStorageNode* obj,const string& fileName){
//Make sure there's nothing left from any previous levels.
assert(levelObjects.empty() && levelObjectsSave.empty() && levelObjectsInitial.empty());
//set current level to loaded one.
currentLevelNode=obj;
//Set the level dimensions to the default, it will probably be changed by the editorData,
//but 800x600 is a fallback.
levelRect = levelRectSaved = levelRectInitial = SDL_Rect{ 0, 0, 800, 600 };
currentCollectables = currentCollectablesSaved = currentCollectablesInitial = 0;
totalCollectables = totalCollectablesSaved = totalCollectablesInitial = 0;
//Load the additional data.
for(map<string,vector<string> >::iterator i=obj->attributes.begin();i!=obj->attributes.end();++i){
if(i->first=="size"){
//We found the size attribute.
if(i->second.size()>=2){
//Set the dimensions of the level.
int w = atoi(i->second[0].c_str()), h = atoi(i->second[1].c_str());
levelRect = levelRectSaved = levelRectInitial = SDL_Rect{ 0, 0, w, h };
}
}else if(i->second.size()>0){
//Any other data will be put into the editorData.
editorData[i->first]=i->second[0];
}
}
//Get the arcade property.
{
string &s = editorData["arcade"];
arcade = atoi(s.c_str()) != 0;
}
//Get the theme.
{
//NOTE: level themes are always enabled.
//Check for bundled (partial) themes for level pack.
if (levels->customTheme){
if (objThemes.appendThemeFromFile(levels->levelpackPath + "/theme/theme.mnmstheme", imageManager, renderer) == NULL){
//The theme failed to load so set the customTheme boolean to false.
levels->customTheme = false;
}
}
//Check for the theme to use for this level. This has higher priority.
//Examples: %DATA%/themes/classic or %USER%/themes/Orange
string &s = editorData["theme"];
if (!s.empty()){
customTheme = objThemes.appendThemeFromFile(processFileName(s) + "/theme.mnmstheme", imageManager, renderer);
}
//Set the Appearance of the player and the shadow.
objThemes.getCharacter(false)->createInstance(&player.appearance, "standright");
player.appearanceInitial = player.appearanceSave = player.appearance;
objThemes.getCharacter(true)->createInstance(&shadow.appearance, "standright");
shadow.appearanceInitial = shadow.appearanceSave = shadow.appearance;
}
//Get the music.
reloadMusic();
//Load the data from the level node.
for(unsigned int i=0;i<obj->subNodes.size();i++){
TreeStorageNode* obj1=obj->subNodes[i];
if(obj1==NULL) continue;
if(obj1->name=="tile"){
Block* block=new Block(this);
if(!block->loadFromNode(imageManager,renderer,obj1)){
delete block;
continue;
}
//If the type is collectable, increase the number of totalCollectables
if (block->type == TYPE_COLLECTABLE) {
totalCollectablesSaved = totalCollectablesInitial = ++totalCollectables;
}
//Add the block to the levelObjects vector.
levelObjects.push_back(block);
}else if(obj1->name=="scenerylayer" && obj1->value.size()==1){
std::string layerName = obj1->value[0];
//Upgrade the layer naming convention.
if (layerName >= "f") {
//Foreground layer.
if (layerName.size() < 3 || layerName[0] != 'f' || layerName[1] != 'g' || layerName[2] != '_') {
layerName = "fg_" + layerName;
}
} else {
//Background layer.
if (layerName.size() < 3 || layerName[0] != 'b' || layerName[1] != 'g' || layerName[2] != '_') {
layerName = "bg_" + layerName;
}
}
//Check if the layer exists.
if (sceneryLayers[layerName] == NULL) {
sceneryLayers[layerName] = new SceneryLayer();
}
//Load contents from node.
sceneryLayers[layerName]->loadFromNode(this, imageManager, renderer, obj1);
}else if(obj1->name=="script" && !obj1->value.empty()){
map<string,int>::iterator it=Game::levelEventNameMap.find(obj1->value[0]);
if(it!=Game::levelEventNameMap.end()){
int eventType=it->second;
const std::string& script=obj1->attributes["script"][0];
if(!script.empty()) scripts[eventType]=script;
}
}
}
//Set the levelName to the name of the current level.
levelName=editorData["name"];
levelFile=fileName;
//Some extra stuff only needed when not in the levelEditor.
if(stateID!=STATE_LEVEL_EDITOR){
//We create a text with the text "Level <levelno> <levelName>".
//It will be shown in the left bottom corner of the screen.
string s;
if(levels->getLevelCount()>1 && levels->type!=COLLECTION){
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),editorData["name"]));
} else {
s = _CC(levels->getDictionaryManager(), editorData["name"]);
}
bmTipsLevelName=textureFromText(renderer, *fontText,s.c_str(),objThemes.getTextColor(true));
}
//Get the background
background=objThemes.getBackground(false);
//Now the loading is finished, we reset all objects to their initial states.
//Before doing that we swap the levelObjects to levelObjectsInitial.
std::swap(levelObjects, levelObjectsInitial);
reset(true, stateID == STATE_LEVEL_EDITOR);
}
void Game::loadLevel(ImageManager& imageManager,SDL_Renderer& renderer,std::string fileName){
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode *obj=new TreeStorageNode();
{
POASerializer objSerializer;
string s=fileName;
//Parse the file.
if(!objSerializer.loadNodeFromFile(s.c_str(),obj,true)){
cerr<<"ERROR: Can't load level file "<<s<<endl;
delete obj;
return;
}
}
//Now call another function.
loadLevelFromNode(imageManager,renderer,obj,fileName);
}
void Game::saveRecord(const char* fileName){
//check if current level is NULL (which should be impossible)
if(currentLevelNode==NULL) return;
TreeStorageNode obj;
POASerializer objSerializer;
//put current level to the node.
currentLevelNode->name="map";
obj.subNodes.push_back(currentLevelNode);
//put the random seed into the attributes.
obj.attributes["seed"].push_back(prngSeed);
//serialize the game record using RLE compression.
#define PUSH_BACK \
if(j>0){ \
if(j>1){ \
sprintf(c,"%d*%d",last,j); \
}else{ \
sprintf(c,"%d",last); \
} \
v.push_back(c); \
}
vector<string> &v=obj.attributes["record"];
vector<int> *record=player.getRecord();
char c[64];
int i,j=0,last;
for(i=0;i<(int)record->size();i++){
int currentKey=(*record)[i];
if(j==0 || currentKey!=last){
PUSH_BACK;
last=currentKey;
j=1;
}else{
j++;
}
}
PUSH_BACK;
#undef PUSH_BACK
#ifdef RECORD_FILE_DEBUG
//add record file debug data.
{
obj.attributes["recordKeyPressLog"].push_back(player.keyPressLog());
vector<SDL_Rect> &playerPosition=player.playerPosition();
string s;
char c[32];
sprintf(c,"%d\n",int(playerPosition.size()));
s=c;
for(unsigned int i=0;i<playerPosition.size();i++){
SDL_Rect& r=playerPosition[i];
sprintf(c,"%d %d\n",r.x,r.y);
s+=c;
}
obj.attributes["recordPlayerPosition"].push_back(s);
}
#endif
//save it
objSerializer.saveNodeToFile(fileName,&obj,true,true);
//remove current level from node to prevent delete it.
obj.subNodes.clear();
}
void Game::loadRecord(ImageManager& imageManager, SDL_Renderer& renderer, const char* fileName, const char* levelFileName) {
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode obj;
{
POASerializer objSerializer;
//Parse the file.
if(!objSerializer.loadNodeFromFile(fileName,&obj,true)){
cerr<<"ERROR: Can't load record file "<<fileName<<endl;
return;
}
}
//Load the seed of psuedo-random number generator.
prngSeed.clear();
{
auto it = obj.attributes.find("seed");
if (it != obj.attributes.end() && !it->second.empty()) {
prngSeed = it->second[0];
}
}
bool loaded=false;
//substitute the level if the level file name is specified.
if (levelFileName) {
TreeStorageNode *obj = new TreeStorageNode();
if (!POASerializer().loadNodeFromFile(levelFileName, obj, true)) {
cerr << "ERROR: Can't load level file " << levelFileName << endl;
delete obj;
} else {
loadLevelFromNode(imageManager, renderer, obj, "?record?");
loaded = true;
}
} else {
//find the node named 'map'.
for (unsigned int i = 0; i < obj.subNodes.size(); i++) {
if (obj.subNodes[i]->name == "map") {
//load the level. (fileName=???)
loadLevelFromNode(imageManager, renderer, obj.subNodes[i], "?record?");
//remove this node to prevent delete it.
obj.subNodes[i] = NULL;
//over
loaded = true;
break;
}
}
}
if(!loaded){
cerr<<"ERROR: Can't find subnode named 'map' from record file"<<endl;
return;
}
//load the record.
{
vector<int> *record=player.getRecord();
record->clear();
vector<string> &v=obj.attributes["record"];
for(unsigned int i=0;i<v.size();i++){
string &s=v[i];
string::size_type pos=s.find_first_of('*');
if(pos==string::npos){
//1 item only.
int i=atoi(s.c_str());
record->push_back(i);
}else{
//contains many items.
int i=atoi(s.substr(0,pos).c_str());
int j=atoi(s.substr(pos+1).c_str());
for(;j>0;j--){
record->push_back(i);
}
}
}
}
#ifdef RECORD_FILE_DEBUG
//load the debug data
{
vector<string> &v=obj.attributes["recordPlayerPosition"];
vector<SDL_Rect> &playerPosition=player.playerPosition();
playerPosition.clear();
if(!v.empty()){
if(!v[0].empty()){
stringstream st(v[0]);
int m;
st>>m;
for(int i=0;i<m;i++){
SDL_Rect r;
st>>r.x>>r.y;
r.w=0;
r.h=0;
playerPosition.push_back(r);
}
}
}
}
#endif
//play the record.
//TODO: tell the level manager don't save the level progress.
player.playRecord();
shadow.playRecord(); //???
}
/////////////EVENT///////////////
void Game::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//First of all let the player handle input.
player.handleInput(&shadow);
//Check for an SDL_QUIT event.
if(event.type==SDL_QUIT){
//We need to quit so enter STATE_EXIT.
setNextState(STATE_EXIT);
}
//Check for the escape key.
if(stateID != STATE_LEVEL_EDITOR && inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
//Escape means we go one level up, to the level select state.
setNextState(STATE_LEVEL_SELECT);
//Save the progress.
levels->saveLevelProgress();
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}
//Check if 'R' is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_RESTART)){
//Restart game only if we are not watching a replay.
if (!player.isPlayFromRecord() || interlevel) {
//Reset the game at next frame.
isReset = true;
//Also delete any gui (most likely the interlevel gui). Only in game mode.
if (GUIObjectRoot && stateID != STATE_LEVEL_EDITOR){
delete GUIObjectRoot;
GUIObjectRoot = NULL;
}
//And set interlevel to false.
interlevel = false;
}
}
//Check for the next level buttons when in the interlevel popup.
if (inputMgr.isKeyDownEvent(INPUTMGR_SPACE) || inputMgr.isKeyDownEvent(INPUTMGR_SELECT)){
if(interlevel){
//The interlevel popup is shown so we need to delete it.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Now goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
//Check if tab is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_TAB)){
//Switch the camera mode.
switch(cameraMode){
case CAMERA_PLAYER:
cameraMode=CAMERA_SHADOW;
break;
case CAMERA_SHADOW:
case CAMERA_CUSTOM:
cameraMode=CAMERA_PLAYER;
break;
}
}
}
/////////////////LOGIC///////////////////
void Game::logic(ImageManager& imageManager, SDL_Renderer& renderer){
//Add one tick to the time.
time++;
//NOTE: This code reverts some changes in commit 5f03ae5.
//This is part of old prepareFrame() code.
//This is needed since otherwise the script function block:setLocation() and block:moveTo() are completely broken.
//Later we should rewrite collision system completely which will remove this piece of code.
//NOTE: In new collision system the effect of dx/dy/xVel/yVel should only be restricted in one frame.
for (auto obj : levelObjects) {
switch (obj->type) {
default:
obj->dx = obj->dy = obj->xVel = obj->yVel = 0;
break;
case TYPE_PUSHABLE:
//NOTE: Currently the dx/dy/etc. of pushable blocks are still carry across frames, in order to make the collision system work correct.
break;
case TYPE_CONVEYOR_BELT: case TYPE_SHADOW_CONVEYOR_BELT:
//NOTE: We let the conveyor belt itself to reset its xVel/yVel.
obj->dx = obj->dy = 0;
break;
}
}
//NOTE2: The above code breaks pushable block with moving block in most cases,
//more precisely, if the pushable block is processed before the moving block then things may be broken.
//Therefore later we must process other blocks before moving pushable block.
//Process delay execution scripts.
getScriptExecutor()->processDelayExecution();
//Process any event in the queue.
for(unsigned int idx=0;idx<eventQueue.size();idx++){
//Get the event from the queue.
typeGameObjectEvent &e=eventQueue[idx];
//Check if the it has an id attached to it.
if(e.target){
//NOTE: Should we check if the target still exists???
e.target->onEvent(e.eventType);
}else if(e.flags&1){
//Loop through the levelObjects and give them the event if they have the right id.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
if(levelObjects[i]->id==e.id){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}else{
//Loop through the levelObjects and give them the event.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}
//Done processing the events so clear the queue.
eventQueue.clear();
//Remove levelObjects whose isDelete is true.
{
int j = 0;
for (int i = 0; i < (int)levelObjects.size(); i++) {
if (levelObjects[i] == NULL) {
j++;
} else if (levelObjects[i]->isDelete) {
delete levelObjects[i];
levelObjects[i] = NULL;
j++;
} else if (j > 0) {
levelObjects[i - j] = levelObjects[i];
}
}
if (j > 0) levelObjects.resize(levelObjects.size() - j);
}
//Check if we should save/load state.
//NOTE: This happens after event handling so no eventQueue has to be saved/restored.
if(saveStateNextTime){
saveState();
}else if(loadStateNextTime){
loadState();
}
saveStateNextTime=false;
loadStateNextTime=false;
//Loop through the gameobjects to update them.
for(unsigned int i=0;i<levelObjects.size();i++){
//Send GameObjectEvent_OnEnterFrame event to the script
levelObjects[i]->onEvent(GameObjectEvent_OnEnterFrame);
}
//Let the gameobject handle movement.
{
std::vector<Block*> pushableBlocks;
//First we process blocks which are not pushable blocks.
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->move();
}
}
//Sort pushable blocks by their position, which is an ad-hoc workaround for
//<https://forum.freegamedev.net/viewtopic.php?f=48&t=8047#p77692>.
std::stable_sort(pushableBlocks.begin(), pushableBlocks.end(),
[](const Block* obj1, const Block* obj2)->bool
{
SDL_Rect r1 = const_cast<Block*>(obj1)->getBox(), r2 = const_cast<Block*>(obj2)->getBox();
if (r1.y > r2.y) return true;
else if (r1.y < r2.y) return false;
else return r1.x < r2.x;
});
//Now we process pushable blocks.
for (auto o : pushableBlocks) {
o->move();
}
}
//Also update the scenery.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it){
it->second->updateAnimation();
}
//Let the player store his move, if recording.
player.shadowSetState();
//Let the player give his recording to the shadow, if configured.
player.shadowGiveState(&shadow);
//NOTE: to fix bugs regarding player/shadow swap, we should first process collision of player/shadow then move them
const SDL_Rect playerLastPosition = player.getBox();
const SDL_Rect shadowLastPosition = shadow.getBox();
//NOTE: The following is ad-hoc code to fix shadow on blocked player on conveyor belt bug
if (shadow.holdingOther) {
//We need to process shadow collision first if shadow is holding player.
//Let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow.
shadow.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = shadow.getBox();
//Check collision for player. Transfer the velocity of shadow to it only if the shadow moves its position.
player.collision(levelObjects, (r.x != shadowLastPosition.x || r.y != shadowLastPosition.y) ? &shadow : NULL);
} else {
//Otherwise we process player first.
//Check collision for player.
player.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = player.getBox();
//Now let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow. Transfer the velocity of player to it only if the player moves its position.
shadow.collision(levelObjects, (r.x != playerLastPosition.x || r.y != playerLastPosition.y) ? &player : NULL);
}
//Let the player move.
player.move(levelObjects, playerLastPosition.x, playerLastPosition.y);
//Let the shadow move.
shadow.move(levelObjects, shadowLastPosition.x, shadowLastPosition.y);
//Check collision and stuff for the shadow and player.
player.otherCheck(&shadow);
//Update the camera.
switch(cameraMode){
case CAMERA_PLAYER:
player.setMyCamera();
break;
case CAMERA_SHADOW:
shadow.setMyCamera();
break;
case CAMERA_CUSTOM:
//NOTE: The target is (should be) screen size independent so calculate the real target x and y here.
int targetX=cameraTarget.x-(SCREEN_WIDTH/2);
int targetY=cameraTarget.y-(SCREEN_HEIGHT/2);
//Move the camera to the cameraTarget.
if(camera.x>targetX){
camera.x-=(camera.x-targetX)>>4;
//Make sure we don't go too far.
if(camera.x<targetX)
camera.x=targetX;
}else if(camera.x<targetX){
camera.x+=(targetX-camera.x)>>4;
//Make sure we don't go too far.
if(camera.x>targetX)
camera.x=targetX;
}
if(camera.y>targetY){
camera.y-=(camera.y-targetY)>>4;
//Make sure we don't go too far.
if(camera.y<targetY)
camera.y=targetY;
}else if(camera.y<targetY){
camera.y+=(targetY-camera.y)>>4;
//Make sure we don't go too far.
if(camera.y>targetY)
camera.y=targetY;
}
break;
}
//Check if we won.
if(won){
//Check if it's level editor test play
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->updateRecordInPlayMode(imageManager, renderer);
}
}
//Check if it's playing from record
else if(player.isPlayFromRecord() && !interlevel){
recordingEnded(imageManager,renderer);
}else{
//Local copy of interlevel property since the replayPlay() will change it later.
const bool previousInterlevel = interlevel;
//We only update the level statistics when the previous state is not interlevel mode.
if (!previousInterlevel) {
//the string to store auto-save record path.
string bestTimeFilePath, bestRecordingFilePath;
//and if we can't get test path.
bool filePathError = false;
//Get current level
LevelPack::Level *level = levels->getLevel();
//Get previous medal
const int oldMedal = level->getMedal();
//Check if MD5 is changed
bool md5Changed = false;
for (int i = 0; i < 16; i++) {
if (level->md5Digest[i] != level->md5InLevelProgress[i]) {
md5Changed = true;
break;
}
}
//Erase existing record if MD5 is changed
if (md5Changed) {
//print some debug message
#if _DEBUG
cout << "MD5 is changed, old " << Md5::toString(level->md5InLevelProgress);
cout << ", new " << Md5::toString(level->md5Digest) << endl;
#endif
level->time = -1;
level->recordings = -1;
//Update the MD5 in record
memcpy(level->md5InLevelProgress, level->md5Digest, sizeof(level->md5Digest));
}
//Get better time and recordings
const int betterTime = level->getBetterTime(time);
const int betterRecordings = level->getBetterRecordings(level->arcade ? currentCollectables : recordings);
//Get new medal
const int newMedal = level->getMedal(betterTime, betterRecordings);
//Check if we need to update statistics
if (newMedal > oldMedal || md5Changed) {
//Erase statictics for old medal
if (oldMedal > 0) statsMgr.completedLevels--;
if (oldMedal == 2) statsMgr.silverLevels--;
if (oldMedal == 3) statsMgr.goldLevels--;
//Update statistics for new medal
if (newMedal > 0) statsMgr.completedLevels++;
if (newMedal == 2) statsMgr.silverLevels++;
if (newMedal == 3) statsMgr.goldLevels++;
}
//Check the achievement "Complete a level with checkpoint, but without saving"
if (objLastCheckPoint.get() == NULL) {
for (auto obj : levelObjects) {
if (obj->type == TYPE_CHECKPOINT) {
statsMgr.newAchievement("withoutsave");
break;
}
}
}
//Set the current level won.
level->won = true;
if (level->time != betterTime) {
level->time = betterTime;
//save the best-time game record.
if (bestTimeFilePath.empty()){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath, bestRecordingFilePath, true);
}
if (bestTimeFilePath.empty()){
cerr << "ERROR: Couldn't get auto-save record file path" << endl;
filePathError = true;
} else{
saveRecord(bestTimeFilePath.c_str());
}
}
if (level->recordings != betterRecordings) {
level->recordings = betterRecordings;
//save the best-recordings game record.
if (bestRecordingFilePath.empty() && !filePathError){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath, bestRecordingFilePath, true);
}
if (bestRecordingFilePath.empty()){
cerr << "ERROR: Couldn't get auto-save record file path" << endl;
filePathError = true;
} else{
saveRecord(bestRecordingFilePath.c_str());
}
}
//Set the next level unlocked if it exists.
if (levels->getCurrentLevel() + 1 < levels->getLevelCount()){
levels->setLocked(levels->getCurrentLevel() + 1);
}
//And save the progress.
levels->saveLevelProgress();
}
//Now go to the interlevel screen.
replayPlay(imageManager,renderer);
//Update achievements (only when the previous state is not interlevel mode)
if (!previousInterlevel) {
if (levels->levelpackName == "tutorial") statsMgr.updateTutorialAchievements();
statsMgr.updateLevelAchievements();
}
//NOTE: We set isReset false to prevent the user from getting a best time of 0.00s and 0 recordings.
isReset = false;
}
}
won=false;
//Check if we should reset.
if (isReset) {
//NOTE: we don't need to reset save ??? it looks like that there are no bugs
reset(false, false);
}
isReset=false;
}
/////////////////RENDER//////////////////
void Game::render(ImageManager&,SDL_Renderer &renderer){
//First of all render the background.
{
//Get a pointer to the background.
ThemeBackground* bg=background;
//Check if the background is null, but there are themes.
if(bg==NULL && objThemes.themeCount()>0){
//Get the background from the first theme in the stack.
bg=objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
if(bg){
//It isn't so draw it.
bg->draw(renderer);
//And if it's the loaded background then also update the animation.
//FIXME: Updating the animation in the render method?
if(bg==background)
bg->updateAnimation();
}else{
//There's no background so fill the screen with white.
SDL_SetRenderDrawColor(&renderer, 255,255,255,255);
SDL_RenderClear(&renderer);
}
}
//Now draw the blackground layers.
auto it = sceneryLayers.begin();
for (; it != sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
it->second->show(renderer);
}
//Now we draw the levelObjects.
{
//NEW: always render the pushable blocks in front of other blocks
std::vector<Block*> pushableBlocks;
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->show(renderer);
}
}
for (auto o : pushableBlocks) {
o->show(renderer);
}
}
//Followed by the player and the shadow.
//NOTE: We draw the shadow first, because he needs to be behind the player.
shadow.show(renderer);
player.show(renderer);
//Now draw the foreground layers.
for (; it != sceneryLayers.end(); ++it){
it->second->show(renderer);
}
//Show the levelName if it isn't the level editor.
if(stateID!=STATE_LEVEL_EDITOR && bmTipsLevelName!=NULL && !interlevel){
SDL_Rect r(rectFromTexture(bmTipsLevelName));
drawGUIBox(-2, SCREEN_HEIGHT - r.h - 4, r.w + 8, r.h + 6, renderer, 0xFFFFFFFF);
applyTexture(2, SCREEN_HEIGHT - r.h, *bmTipsLevelName, renderer, NULL);
}
//Check if there's a tooltip.
if(!gameTipText.empty()){
std::string message = gameTipText;
if (gameTipTexture.needsUpdate(gameTipText)) {
int maxWidth = 0, y = 0;
std::vector<std::string> string_data;
//Trim the message.
{
size_t lps = message.find_first_not_of("\n\r \t");
if (lps == string::npos) {
message.clear(); // it's completely empty
} else {
message = message.substr(lps, message.find_last_not_of("\n\r \t") - lps + 1);
}
}
if (!message.empty()) {
+ message.push_back('\0');
+
//Split the message into lines.
for (int lps = 0;;) {
// determine the end of line
int lpe = lps;
for (; message[lpe] != '\n' && message[lpe] != '\r' && message[lpe] != '\0'; lpe++);
string_data.push_back(message.substr(lps, lpe - lps));
// break if the string ends
if (message[lpe] == '\0') break;
// skip "\r\n" for Windows line ending
if (message[lpe] == '\r' && message[lpe + 1] == '\n') lpe++;
// point to the start of next line
lps = lpe + 1;
}
vector<SurfacePtr> lines;
//Create the image for each lines
for (int i = 0; i < (int)string_data.size(); i++) {
//Integer used to center the sentence horizontally.
int w = 0, h = 0;
TTF_SizeUTF8(fontText, string_data[i].c_str(), &w, &h);
//Find out largest width
if (w > maxWidth)
maxWidth = w;
lines.emplace_back(TTF_RenderUTF8_Blended(fontText, string_data[i].c_str(), objThemes.getTextColor(true)));
//Increase y with the height of the text.
y += h;
}
SurfacePtr surf = createSurface(maxWidth, y);
y = 0;
for (auto &s : lines) {
if (s) {
applySurface(0, y, s.get(), surf.get(), NULL);
y += s->h;
}
}
gameTipTexture.update(gameTipText, textureUniqueFromSurface(renderer, std::move(surf)));
}
}
//We already have a gameTip for this type so draw it.
if (!message.empty() && gameTipTexture.get()){
SDL_Rect r(rectFromTexture(*gameTipTexture.get()));
drawGUIBox(-2, -2, r.w + 8, r.h + 6, renderer, 0xFFFFFFFF);
applyTexture(2, 2, *gameTipTexture.get(), renderer);
}
}
//Reset the gameTip.
gameTipText.clear();
// Limit the scope of bm, as it's a borrowed pointer.
{
//Pointer to the sdl texture that will contain a message, if any.
SDL_Texture* bm=NULL;
//Check if the player is dead, meaning we draw a message.
if(player.dead){
//Get user configured restart key
string keyCodeRestart = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_RESTART, false), inputMgr.getKeyCode(INPUTMGR_RESTART, true));
//The player is dead, check if there's a state that can be loaded.
if(player.canLoadState()){
//Now check if the tip is already made, if not make it.
if(bmTipsRestartCheckpoint==NULL){
//Get user defined key for loading checkpoint
string keyCodeLoad = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_LOAD, false), inputMgr.getKeyCode(INPUTMGR_LOAD, true));
//Draw string
bmTipsRestartCheckpoint = textureFromText(renderer, *fontText,//TTF_RenderUTF8_Blended(fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - first %s means currently configured key to restart game
/// - Second %s means configured key to load from last save
tfm::format(_("Press %s to restart current level or press %s to load the game."),
keyCodeRestart,keyCodeLoad).c_str(),
objThemes.getTextColor(true));
}
bm = bmTipsRestartCheckpoint.get();
}else{
//Now check if the tip is already made, if not make it.
if (bmTipsRestart == NULL){
bmTipsRestart = textureFromText(renderer, *fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with currently configured key to restart game
tfm::format(_("Press %s to restart current level."),keyCodeRestart).c_str(),
objThemes.getTextColor(true));
}
bm = bmTipsRestart.get();
}
}
//Check if the shadow has died (and there's no other notification).
//NOTE: We use the shadow's jumptime as countdown, this variable isn't used when the shadow is dead.
if(shadow.dead && bm==NULL && shadow.jumpTime>0){
//Now check if the tip is already made, if not make it.
if(bmTipsShadowDeath==NULL){
bmTipsShadowDeath = textureFromText(renderer, *fontText,
_("Your shadow has died."),
objThemes.getTextColor(true));
}
bm = bmTipsShadowDeath.get();
//NOTE: Logic in the render loop, we substract the shadow's jumptime by one.
shadow.jumpTime--;
//return view to player and keep it there
cameraMode=CAMERA_PLAYER;
}
//Draw the tip.
if(bm!=NULL){
const SDL_Rect textureSize = rectFromTexture(*bm);
int x=(SCREEN_WIDTH-textureSize.w)/2;
int y=32;
drawGUIBox(x-8,y-8,textureSize.w+16,textureSize.h+14,renderer,0xFFFFFFFF);
applyTexture(x,y,*bm,renderer);
}
}
{
int y = SCREEN_HEIGHT;
//Show the number of collectables the user has collected if there are collectables in the level.
//We hide this when interlevel.
if ((currentCollectables || totalCollectables) && !interlevel){
if (arcade) {
//Only show the current collectibles in arcade mode.
if (collectablesTexture.needsUpdate(currentCollectables)) {
collectablesTexture.update(currentCollectables,
textureFromText(renderer,
*fontText,
tfm::format("%d", currentCollectables).c_str(),
objThemes.getTextColor(true)));
}
} else {
if (collectablesTexture.needsUpdate(currentCollectables ^ (totalCollectables << 16))) {
collectablesTexture.update(currentCollectables ^ (totalCollectables << 16),
textureFromText(renderer,
*fontText,
tfm::format("%d/%d", currentCollectables, totalCollectables).c_str(),
objThemes.getTextColor(true)));
}
}
SDL_Rect bmSize = rectFromTexture(*collectablesTexture.get());
//Draw background
drawGUIBox(SCREEN_WIDTH - bmSize.w - 34, SCREEN_HEIGHT - bmSize.h - 4, bmSize.w + 34 + 2, bmSize.h + 4 + 2, renderer, 0xFFFFFFFF);
//Draw the collectable icon
collectable.draw(renderer, SCREEN_WIDTH - 50 + 12, SCREEN_HEIGHT - 50 + 10);
//Draw text
applyTexture(SCREEN_WIDTH - 50 - bmSize.w + 22, SCREEN_HEIGHT - bmSize.h, collectablesTexture.getTexture(), renderer);
y -= bmSize.h + 4;
}
//Show additional message in level editor.
if (stateID == STATE_LEVEL_EDITOR && additionalTexture) {
SDL_Rect r = rectFromTexture(*additionalTexture.get());
r.x = SCREEN_WIDTH - r.w;
r.y = y - r.h;
SDL_SetRenderDrawColor(&renderer, 255, 255, 255, 255);
SDL_RenderFillRect(&renderer, &r);
applyTexture(r.x, r.y, additionalTexture, renderer);
}
}
//show time and records used in level editor or during replay or in arcade mode.
if ((stateID == STATE_LEVEL_EDITOR || (!interlevel && (player.isPlayFromRecord() || arcade)))){
const SDL_Color fg=objThemes.getTextColor(true),bg={255,255,255,255};
const int alpha = 160;
//don't show number of records in arcade mode
if (!arcade && recordingsTexture.needsUpdate(recordings)) {
recordingsTexture.update(recordings,
textureFromTextShaded(
renderer,
*fontMono,
tfm::format(ngettext("%d recording","%d recordings",recordings).c_str(),recordings).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(recordingsTexture.get(),alpha);
}
int y = SCREEN_HEIGHT - (arcade ? 0 : textureHeight(*recordingsTexture.get()));
if (stateID != STATE_LEVEL_EDITOR && bmTipsLevelName != NULL && !interlevel) {
y -= textureHeight(bmTipsLevelName) + 4;
}
if (!arcade) applyTexture(0,y,*recordingsTexture.get(), renderer);
if(timeTexture.needsUpdate(time)) {
timeTexture.update(time,
textureFromTextShaded(
renderer,
*fontMono,
tfm::format("%-.2fs", time / 40.0).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(timeTexture.get(), alpha);
}
y -= textureHeight(*timeTexture.get());
applyTexture(0,y,*timeTexture.get(), renderer);
}
//Draw the current action in the upper right corner.
if(player.record){
const SDL_Rect r = { 0, 0, 50, 50 };
applyTexture(SCREEN_WIDTH - 50, 0, *action, renderer, &r);
} else if (shadow.state != 0){
const SDL_Rect r={50,0,50,50};
applyTexture(SCREEN_WIDTH-50,0,*action,renderer,&r);
}
//if the game is play from record then draw something indicates it
if(player.isPlayFromRecord()){
//Dim the screen if interlevel is true.
if( interlevel){
dimScreen(renderer,191);
}else if((time & 0x10)==0x10){
// FIXME: replace this ugly ad-hoc animation by a better one
const SDL_Rect r={50,0,50,50};
applyTexture(0,0,*action,renderer,&r);
//applyTexture(0,SCREEN_HEIGHT-50,*action,renderer,&r);
//applyTexture(SCREEN_WIDTH-50,SCREEN_HEIGHT-50,*action,renderer,&r);
}
}else if(auto blockId = player.objNotificationBlock.get()){
//If the player is in front of a notification block show the message.
//And it isn't a replay.
//Check if we need to update the notification message texture.
int maxWidth = 0;
int y = 20;
//We check against blockId rather than the full message, as blockId is most likely shorter.
if(notificationTexture.needsUpdate(blockId)) {
std::string message = translateAndExpandMessage(blockId->message);
std::vector<std::string> string_data;
//Trim the message.
{
size_t lps = message.find_first_not_of("\n\r \t");
if (lps == string::npos) {
message.clear(); // it's completely empty
} else {
message = message.substr(lps, message.find_last_not_of("\n\r \t") - lps + 1);
}
}
//Split the message into lines.
for (int lps = 0;;) {
// determine the end of line
int lpe = lps;
for (; message[lpe] != '\n' && message[lpe] != '\r' && message[lpe] != '\0'; lpe++);
string_data.push_back(message.substr(lps, lpe - lps));
// break if the string ends
if (message[lpe] == '\0') break;
// skip "\r\n" for Windows line ending
if (message[lpe] == '\r' && message[lpe + 1] == '\n') lpe++;
// point to the start of next line
lps = lpe + 1;
}
vector<SurfacePtr> lines;
//Create the image for each lines
for (int i = 0; i < (int)string_data.size(); i++) {
//Integer used to center the sentence horizontally.
int x = 0;
TTF_SizeUTF8(fontText, string_data[i].c_str(), &x, NULL);
//Find out largest width
if (x>maxWidth)
maxWidth = x;
lines.emplace_back(TTF_RenderUTF8_Blended(fontText, string_data[i].c_str(), objThemes.getTextColor(true)));
//Increase y with 25, about the height of the text.
y += 25;
}
maxWidth+=SCREEN_WIDTH*0.15;
SurfacePtr surf = createSurface(maxWidth, y);
int y1 = y;
for(SurfacePtr &s : lines) {
if(s) {
applySurface((surf->w-s->w)/2,surf->h - y1,s.get(),surf.get(),NULL);
}
y1 -= 25;
}
notificationTexture.update(blockId, textureUniqueFromSurface(renderer,std::move(surf)));
} else {
auto texSize = rectFromTexture(*notificationTexture.get());
maxWidth=texSize.w;
y=texSize.h;
}
drawGUIBox((SCREEN_WIDTH-maxWidth)/2,SCREEN_HEIGHT-y-25,maxWidth,y+20,renderer,0xFFFFFFBF);
applyTexture((SCREEN_WIDTH-maxWidth)/2,SCREEN_HEIGHT-y,notificationTexture.getTexture(),renderer);
}
}
std::string Game::translateAndExpandMessage(const std::string &untranslated_message) {
std::string message = _CC(levels->getDictionaryManager(), untranslated_message);
//Expand the variables.
std::map<std::string, std::string> cachedVariables;
for (;;) {
size_t lps = message.find("{{{");
if (lps == std::string::npos) break;
size_t lpe = message.find("}}}", lps);
if (lpe == std::string::npos) break;
std::string varName = message.substr(lps + 3, lpe - lps - 3), varValue;
auto it = cachedVariables.find(varName);
if (it != cachedVariables.end()) {
varValue = it->second;
} else {
bool isUnknown = true;
if (varName.size() >= 4 && varName.substr(0, 4) == "key_") {
//Probably a key name.
InputManagerKeys key = InputManager::getKeyFromName(varName);
if (key != INPUTMGR_MAX) {
varValue = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(key, false), inputMgr.getKeyCode(key, true));
isUnknown = false;
}
}
if (isUnknown) {
//Unknown variable
cerr << "Warning: Unknown variable '{{{" << varName << "}}}' in notification block message!" << endl;
}
cachedVariables[varName] = varValue;
}
//Substitute.
message.replace(message.begin() + lps, message.begin() + (lpe + 3), varValue);
}
//Over.
return message;
}
void Game::resize(ImageManager&, SDL_Renderer& /*renderer*/){
//Check if the interlevel popup is shown.
if(interlevel && GUIObjectRoot){
GUIObjectRoot->left=(SCREEN_WIDTH-GUIObjectRoot->width)/2;
}
}
void Game::replayPlay(ImageManager& imageManager,SDL_Renderer& renderer){
//Set interlevel true.
interlevel=true;
//Make a copy of the playerButtons.
vector<int> recordCopy=player.recordButton;
//Backup of time and recordings, etc. before we reset the level.
//We choose the same variable names so that we don't need to modify the existing code.
const int time = this->time, recordings = this->recordings, currentCollectables = this->currentCollectables;
//Reset the game.
//NOTE: We don't reset the saves. I'll see that if it will introduce bugs.
reset(false, false);
//Make the cursor visible when the interlevel popup is up.
SDL_ShowCursor(SDL_ENABLE);
//Set the copy of playerButtons back.
player.recordButton=recordCopy;
//Now play the recording.
player.playRecord();
//Create the gui if it isn't already done.
if(!GUIObjectRoot){
//Create a new GUIObjectRoot the size of the screen.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Make child widgets change color properly according to theme.
GUIObjectRoot->inDialog=true;
//Create a GUIFrame for the upper frame.
GUIFrame* upperFrame=new GUIFrame(imageManager,renderer,0,4,0,74);
GUIObjectRoot->addChild(upperFrame);
//Render the You've finished: text and add it to a GUIImage.
//NOTE: The texture is managed by the GUIImage so no need to free it ourselfs.
auto bm = SharedTexture(textureFromText(renderer, *fontGUI,_("You've finished:"),objThemes.getTextColor(true)));
const SDL_Rect textureSize = rectFromTexture(*bm);
GUIImage* title=new GUIImage(imageManager,renderer,0,4-GUI_FONT_RAISE,textureSize.w,textureSize.h,bm);
upperFrame->addChild(title);
//Create the sub title.
string s;
if (levels->getLevelCount()>0){
/// TRANSLATORS: Please do not remove %s or %d from your translation:
/// - %d means the level number in a levelpack
/// - %s means the name of current level
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),levelName));
}
GUIObject* obj=new GUILabel(imageManager,renderer,0,44,0,28,s.c_str(),0,true,true,GUIGravityCenter);
upperFrame->addChild(obj);
obj->render(renderer,0,0,false);
//Determine the width the upper frame should have.
int width;
if(textureSize.w>obj->width)
width=textureSize.w+32;
else
width=obj->width+32;
//Set the left of the title.
title->left=(width-title->width)/2;
//Set the width of the level label to the width of the frame for centering.
obj->width=width;
//Now set the position and width of the frame.
upperFrame->width=width;
upperFrame->left=(SCREEN_WIDTH-width)/2;
//Now create a GUIFrame for the lower frame.
GUIFrame* lowerFrame=new GUIFrame(imageManager,renderer,0,SCREEN_HEIGHT-140,570,135);
GUIObjectRoot->addChild(lowerFrame);
//The different values.
int bestTime=levels->getLevel()->time;
int targetTime=levels->getLevel()->targetTime;
int bestRecordings=levels->getLevel()->recordings;
int targetRecordings=levels->getLevel()->targetRecordings;
int medal = levels->getLevel()->getMedal();
assert(medal > 0);
int maxWidth=0;
int x=20;
//Is there a target time for this level?
int timeY=0;
bool isTargetTime=true;
if(targetTime<0){
isTargetTime=false;
timeY=12;
}
//Create the labels with the time and best time.
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,10+timeY,-1,36,tfm::format(_("Time: %-.2fs"),time/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,34+timeY,-1,36,tfm::format(_("Best time: %-.2fs"),bestTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
if(isTargetTime){
obj=new GUILabel(imageManager,renderer,x,58,-1,36,tfm::format(_("Target time: %-.2fs"),targetTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth+20;
//Is there target recordings for this level?
int recsY=0;
bool isTargetRecs=true;
if(targetRecordings<0){
isTargetRecs=false;
recsY=12;
}
//Now the ones for the recordings.
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj = new GUILabel(imageManager, renderer, x, 10 + recsY, -1, 36,
arcade ? tfm::format(_("Collectibles: %d"), currentCollectables).c_str() : tfm::format(_("Recordings: %d"), recordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj = new GUILabel(imageManager, renderer, x, 34 + recsY, -1, 36,
tfm::format(_(arcade ? "Best collectibles: %d" : "Best recordings: %d"), bestRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
if(isTargetRecs){
obj = new GUILabel(imageManager, renderer, x, 58, -1, 36,
tfm::format(_(arcade ? "Target collectibles: %d" : "Target recordings: %d"), targetRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth;
//The medal that is earned.
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with name of a prize medal (gold, silver or bronze)
string s1=tfm::format(_("You earned the %s medal"),(medal>1)?(medal==3)?_("GOLD"):_("SILVER"):_("BRONZE"));
obj=new GUILabel(imageManager,renderer,50,92,-1,36,s1.c_str(),0,true,true,GUIGravityCenter);
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->left+obj->width>x){
x=obj->left+obj->width+30;
}else{
obj->left=20+(x-20-obj->width)/2;
}
//Create the rectangle for the earned medal.
SDL_Rect r;
r.x=(medal-1)*30;
r.y=0;
r.w=30;
r.h=30;
//Create the medal on the left side.
obj=new GUIImage(imageManager,renderer,16,92,30,30,medals,r);
lowerFrame->addChild(obj);
//And the medal on the right side.
obj=new GUIImage(imageManager,renderer,x-24,92,30,30,medals,r);
lowerFrame->addChild(obj);
//Create the three buttons, Menu, Restart, Next.
/// TRANSLATORS: used as return to the level selector menu
GUIObject* b1=new GUIButton(imageManager,renderer,x,10,-1,36,_("Menu"),0,true,true,GUIGravityCenter);
b1->name="cmdMenu";
b1->eventCallback=this;
lowerFrame->addChild(b1);
b1->render(renderer,0,0,true);
/// TRANSLATORS: used as restart level
GUIObject* b2=new GUIButton(imageManager,renderer,x,50,-1,36,_("Restart"),0,true,true,GUIGravityCenter);
b2->name="cmdRestart";
b2->eventCallback=this;
lowerFrame->addChild(b2);
b2->render(renderer,0,0,true);
/// TRANSLATORS: used as next level
GUIObject* b3=new GUIButton(imageManager,renderer,x,90,-1,36,_("Next"),0,true,true,GUIGravityCenter);
b3->name="cmdNext";
b3->eventCallback=this;
lowerFrame->addChild(b3);
b3->render(renderer,0,0,true);
maxWidth=b1->width;
if(b2->width>maxWidth)
maxWidth=b2->width;
if(b3->width>maxWidth)
maxWidth=b3->width;
b1->left=b2->left=b3->left=x+maxWidth/2;
x+=maxWidth;
lowerFrame->width=x;
lowerFrame->left=(SCREEN_WIDTH-lowerFrame->width)/2;
}
}
void Game::recordingEnded(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if it's a normal replay, if so just stop.
if(!interlevel){
//Show the cursor so that the user can press the ok button.
SDL_ShowCursor(SDL_ENABLE);
//Now show the message box.
msgBox(imageManager,renderer,_("Game replay is done."),MsgBoxOKOnly,_("Game Replay"));
//Go to the level select menu.
setNextState(STATE_LEVEL_SELECT);
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}else{
//Instead of directly replaying we set won true to let the Game handle the replaying at the end of the update cycle.
won=true;
}
}
bool Game::canSaveState(){
return (player.canSaveState() && shadow.canSaveState());
}
bool Game::saveState(){
//Check if the player and shadow can save the current state.
if(canSaveState()){
//Let the player and the shadow save their state.
player.saveState();
shadow.saveState();
//Save the stats.
timeSaved=time;
recordingsSaved=recordings;
recentSwapSaved=recentSwap;
//Save the PRNG and seed.
prngSaved = prng;
prngSeedSaved = prngSeed;
//Save the level size.
levelRectSaved = levelRect;
//Save the camera mode and target.
cameraModeSaved=cameraMode;
cameraTargetSaved=cameraTarget;
//Save the current collectables
currentCollectablesSaved = currentCollectables;
totalCollectablesSaved = totalCollectables;
//Save scripts.
copyCompiledScripts(getScriptExecutor()->getLuaState(), compiledScripts, savedCompiledScripts);
//Save other state, for example moving blocks.
copyLevelObjects(levelObjects, levelObjectsSave, false);
//Also save states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->saveAnimation();
}
//Also save the background animation, if any.
if(background)
background->saveAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentSave+1000>t){
statsMgr.newAchievement("panicSave");
}
recentSave=t;
//Update statistics.
statsMgr.saveTimes++;
//Update achievements
switch(statsMgr.saveTimes){
case 100:
statsMgr.newAchievement("save100");
break;
}
}
//Save the state for script executor.
getScriptExecutor()->saveState();
//Execute the onSave event.
executeScript(LevelEvent_OnSave);
//Return true.
return true;
}
//We can't save the state so return false.
return false;
}
bool Game::loadState(){
//Check if there's a state that can be loaded.
if(player.canLoadState() && shadow.canLoadState()){
//Reset the stats for level editor.
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->currentTime = editor->currentRecordings = -1;
editor->updateAdditionalTexture(getImageManager(), getRenderer());
}
}
//Let the player and the shadow load their state.
player.loadState();
shadow.loadState();
//Load the stats.
time=timeSaved;
recordings=recordingsSaved;
recentSwap=recentSwapSaved;
//Load the PRNG and seed.
prng = prngSaved;
prngSeed = prngSeedSaved;
//Load the level size.
levelRect = levelRectSaved;
//Load the camera mode and target.
cameraMode=cameraModeSaved;
cameraTarget=cameraTargetSaved;
//Load the current collactbles
currentCollectables = currentCollectablesSaved;
totalCollectables = totalCollectablesSaved;
//Load scripts.
copyCompiledScripts(getScriptExecutor()->getLuaState(), savedCompiledScripts, compiledScripts);
//Load other state, for example moving blocks.
copyLevelObjects(levelObjectsSave, levelObjects, true);
//Also load states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->loadAnimation();
}
//Also load the background animation, if any.
if(background)
background->loadAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements.
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentLoad+1000>t){
statsMgr.newAchievement("panicLoad");
}
recentLoad=t;
//Update statistics.
statsMgr.loadTimes++;
//Update achievements
switch(statsMgr.loadTimes){
case 100:
statsMgr.newAchievement("load100");
break;
}
}
//Load the state for script executor.
getScriptExecutor()->loadState();
//Execute the onLoad event, if any.
executeScript(LevelEvent_OnLoad);
//Return true.
return true;
}
//We can't load the state so return false.
return false;
}
static std::string createNewSeed() {
static int createSeedTime = 0;
struct Buffer {
time_t systemTime;
Uint32 sdlTicks;
int x;
int y;
int createSeedTime;
} buffer;
buffer.systemTime = time(NULL);
buffer.sdlTicks = SDL_GetTicks();
SDL_GetMouseState(&buffer.x, &buffer.y);
buffer.createSeedTime = ++createSeedTime;
return Md5::toString(Md5::calc(&buffer, sizeof(buffer), NULL));
}
void Game::reset(bool save,bool noScript){
//Some sanity check, i.e. if we switch from no-script mode to script mode, we should always reset the save
assert(noScript || getScriptExecutor() || save);
//We need to reset the game so we also reset the player and the shadow.
player.reset(save);
shadow.reset(save);
saveStateNextTime=false;
loadStateNextTime=false;
//Reset the stats.
time=0;
recordings=0;
recentSwap=-10000;
if(save) recentSwapSaved=-10000;
//Reset the stats for level editor.
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->currentTime = editor->currentRecordings = -1;
editor->updateAdditionalTexture(getImageManager(), getRenderer());
}
}
//Reset the pseudo-random number generator by creating a new seed, unless we are playing from record.
if (levelFile == "?record?" || interlevel) {
if (prngSeed.empty()) {
cout << "WARNING: The record file doesn't provide a random seed! Will create a new random seed!"
"This may breaks the behavior pseudo-random number generator in script!" << endl;
prngSeed = createNewSeed();
} else {
#ifdef _DEBUG
cout << "Use existing PRNG seed: " << prngSeed << endl;
#endif
}
} else {
prngSeed = createNewSeed();
#ifdef _DEBUG
cout << "Create new PRNG seed: " << prngSeed << endl;
#endif
}
{
std::seed_seq seq(prngSeed.begin(), prngSeed.end());
prng.seed(seq);
}
if (save) {
prngSaved = prng;
prngSeedSaved = prngSeed;
}
//Reset the level size.
levelRect = levelRectInitial;
if (save) levelRectSaved = levelRectInitial;
//Reset the camera.
cameraMode=CAMERA_PLAYER;
if(save) cameraModeSaved=CAMERA_PLAYER;
cameraTarget = SDL_Rect{ 0, 0, 0, 0 };
if (save) cameraTargetSaved = SDL_Rect{ 0, 0, 0, 0 };
//Reset the number of collectables
currentCollectables = currentCollectablesInitial;
totalCollectables = totalCollectablesInitial;
if (save) {
currentCollectablesSaved = currentCollectablesInitial;
totalCollectablesSaved = totalCollectablesInitial;
}
//Clear the event queue, since all the events are from before the reset.
eventQueue.clear();
//Reset states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->resetAnimation(save);
}
//Also reset the background animation, if any.
if(background)
background->resetAnimation(save);
//Reset the cached notification block
notificationTexture.update(NULL, NULL);
//Reset the script environment if necessary.
if (noScript) {
//Destroys the script environment completely.
scriptExecutor->destroy();
//Clear the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
//Clear the block script.
for (auto block : levelObjects){
block->compiledScripts.clear();
}
for (auto block : levelObjectsSave){
block->compiledScripts.clear();
}
for (auto block : levelObjectsInitial){
block->compiledScripts.clear();
}
} else if (save) {
//Create a new script environment.
getScriptExecutor()->reset(true);
//Recompile the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
for (auto it = scripts.begin(); it != scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
compiledScripts[it->first] = index;
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
savedCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
initialCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
}
//Recompile the block script.
for (auto block : levelObjects){
block->compiledScripts.clear();
}
for (auto block : levelObjectsSave){
block->compiledScripts.clear();
}
for (auto block : levelObjectsInitial) {
block->compiledScripts.clear();
for (auto it = block->scripts.begin(); it != block->scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
block->compiledScripts[it->first] = index;
}
}
} else {
assert(getScriptExecutor());
//Do a soft reset.
getScriptExecutor()->reset(false);
//Restore the level script to initial state.
copyCompiledScripts(getScriptExecutor()->getLuaState(), initialCompiledScripts, compiledScripts);
//NOTE: We don't need to restore the block script since it will be restored automatically when the block array is copied.
}
//We reset levelObjects here since we need to wait the compiledScripts being initialized.
copyLevelObjects(levelObjectsInitial, levelObjects, true);
if (save) {
copyLevelObjects(levelObjectsInitial, levelObjectsSave, false);
}
//Also reset the last checkpoint so set it to NULL.
if (save)
objLastCheckPoint = NULL;
//Call the level's onCreate event.
executeScript(LevelEvent_OnCreate);
//Send GameObjectEvent_OnCreate event to the script
for (auto block : levelObjects) {
block->onEvent(GameObjectEvent_OnCreate);
}
//Close exit(s) if there are any collectables
if (totalCollectables>0){
for (auto block : levelObjects){
if (block->type == TYPE_EXIT){
block->onEvent(GameObjectEvent_OnSwitchOff);
}
}
}
//Hide the cursor (if not the leveleditor).
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
void Game::executeScript(int eventType){
map<int,int>::iterator it;
//Check if there's a script for the given event.
it=compiledScripts.find(eventType);
if(it!=compiledScripts.end()){
//There is one so execute it.
getScriptExecutor()->executeScript(it->second);
}
}
void Game::broadcastObjectEvent(int eventType,int objectType,const char* id,GameObject* target){
//Create a typeGameObjectEvent that can be put into the queue.
typeGameObjectEvent e;
//Set the event type.
e.eventType=eventType;
//Set the object type.
e.objectType=objectType;
//By default flags=0.
e.flags=0;
//Unless there's an id.
if(id){
//Set flags to 0x1 and set the id.
e.flags|=1;
e.id=id;
}
//Or there's a target given.
if(target)
e.target=target;
else
e.target=NULL;
//Add the event to the queue.
eventQueue.push_back(e);
}
void Game::getCurrentLevelAutoSaveRecordPath(std::string &bestTimeFilePath,std::string &bestRecordingFilePath,bool createPath){
levels->getLevelAutoSaveRecordPath(-1,bestTimeFilePath,bestRecordingFilePath,createPath);
}
void Game::gotoNextLevel(ImageManager& imageManager, SDL_Renderer& renderer){
//Goto the next level.
levels->nextLevel();
//Check if the level exists.
if(levels->getCurrentLevel()<levels->getLevelCount()){
setNextState(STATE_GAME);
}else{
if(!levels->congratulationText.empty()){
msgBox(imageManager,renderer,_CC(levels->getDictionaryManager(),levels->congratulationText),MsgBoxOKOnly,_("Congratulations"));
}else{
msgBox(imageManager,renderer,_("You have finished the levelpack!"),MsgBoxOKOnly,_("Congratulations"));
}
//Now go back to the levelselect screen.
setNextState(STATE_LEVEL_SELECT);
//And set the music back to menu.
getMusicManager()->playMusic("menu");
}
}
void Game::GUIEventCallback_OnEvent(ImageManager& imageManager,SDL_Renderer& renderer, string name,GUIObject* obj,int eventType){
if(name=="cmdMenu"){
setNextState(STATE_LEVEL_SELECT);
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}else if(name=="cmdRestart"){
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
interlevel=false;
//And reset the game.
//NOTE: We don't need to clear the save game because in level replay the game won't be saved (??)
//TODO: it seems work (??); I'll see if it introduce bugs
reset(false, false);
}else if(name=="cmdNext"){
//No matter what, clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//And goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
void Game::invalidateNotificationTexture(Block *block) {
if (block == NULL || block == notificationTexture.getId()) {
notificationTexture.update(NULL, NULL);
}
}
diff --git a/src/LevelEditor.cpp b/src/LevelEditor.cpp
index d8a2b03..fe3af38 100644
--- a/src/LevelEditor.cpp
+++ b/src/LevelEditor.cpp
@@ -1,5086 +1,5086 @@
/*
* 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 "Commands.h"
#include "CommandManager.h"
#include "GameState.h"
#include "Functions.h"
#include "GameObjects.h"
#include "ThemeManager.h"
#include "LevelPack.h"
#include "LevelPackManager.h"
#include "LevelEditor.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "GUIListBox.h"
#include "GUITextArea.h"
#include "GUIWindow.h"
#include "GUISpinBox.h"
#include "MusicManager.h"
#include "InputManager.h"
#include "StatisticsManager.h"
#include <fstream>
#include <iostream>
#include <vector>
#include <algorithm>
#include <string.h>
#include <stdio.h>
#include <sstream>
#include <SDL_ttf.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
//Array containing translateble block names
const char* LevelEditor::blockNames[TYPE_MAX]={
__("Block"),__("Player Start"),__("Shadow Start"),
__("Exit"),__("Shadow Block"),__("Spikes"),
__("Checkpoint"),__("Swap"),__("Fragile"),
__("Moving Block"),__("Moving Shadow Block"),__("Moving Spikes"),
__("Teleporter"),__("Button"),__("Switch"),
__("Conveyor Belt"),__("Shadow Conveyor Belt"),__("Notification Block"),__("Collectable"),__("Pushable")
};
static const std::array<const char*, static_cast<size_t>(ToolTips::TooltipMax)> tooltipNames = {
__("Select"), __("Add"), __("Delete"), __("Play"), "", "", __("Level settings"), __("Save level"), __("Back to menu"),
__("Select"), __("Delete"), __("Configure")
};
static const std::array<const char*, static_cast<size_t>(ToolTips::TooltipMax)> tooltipHotkey = {
"F2", "F3", "F4", "F5", "", "", "", "Ctrl+S", "",
"", "", ""
};
static const std::array<int, static_cast<size_t>(ToolTips::TooltipMax)> tooltipHotkey2 = {
-1, -1, -1, -1, -1, -1, INPUTMGR_TAB, -1, INPUTMGR_ESCAPE,
-1, -1, -1
};
//Array indicates if block is linkable
static const bool isLinkable[TYPE_MAX]={
false,false,false,
false,false,false,
false,false,false,
true,true,true,
true,true,true,
false,false,false,false
};
std::string LevelEditor::describeSceneryName(const std::string& name) {
//Check if the input is a valid block name.
if (name.size() > 8 && name.substr(name.size() - 8) == "_Scenery") {
auto it = Game::blockNameMap.find(name.substr(0, name.size() - 8));
if (it != Game::blockNameMap.end()){
return tfm::format(_("%s (Scenery)"), _(LevelEditor::blockNames[it->second]));
}
}
return name;
}
/////////////////LevelEditorActionsPopup/////////////////
class LevelEditorActionsPopup:private GUIEventCallback{
private:
//The parent object.
LevelEditor* parent;
//The position and size of window.
SDL_Rect rect;
//Array containing the actions in this popup.
GUIListBox* actions;
//GUI image.
SDL_Surface* bmGUI;
//Pointer to the object the actions apply to.
GameObject* target;
//The behaviour names.
vector<string> behaviour;
//The fragile block states.
vector<string> states;
//The separator texture.
//NOTE: It's created only when it's used. So don't access it directly!
SharedTexture separatorTexture;
//Get or create a new separator texture.
SharedTexture getOrCreateSeparatorTexture(SDL_Renderer& renderer) {
if (!separatorTexture) {
//NOTE: we use an arbitrart width since it will be updated soon.
createSeparatorTexture(renderer, 16);
}
return separatorTexture;
}
void createSeparatorTexture(SDL_Renderer& renderer, int width) {
//create surface
SurfacePtr surface = createSurface(width, 5);
//draw horizontal separator
const SDL_Rect r0 = { 4, 1, width - 8, 1 };
const SDL_Rect r1 = { 4, 3, width - 8, 1 };
const SDL_Rect r2 = { 4, 2, width - 8, 1 };
Uint32 c0 = SDL_MapRGB(surface->format, 192, 192, 192);
Uint32 c2 = SDL_MapRGB(surface->format, 64, 64, 64);
SDL_FillRect(surface.get(), &r0, c0);
SDL_FillRect(surface.get(), &r1, c0);
SDL_FillRect(surface.get(), &r2, c2);
//over
separatorTexture = textureFromSurface(renderer, std::move(surface));
}
void updateSeparators(SDL_Renderer& renderer) {
createSeparatorTexture(renderer, rect.w);
for (unsigned int i = 0; i < actions->item.size(); i++) {
if (actions->item[i] == "-") {
actions->updateItem(renderer, i, "-", separatorTexture);
}
}
}
public:
SDL_Rect getRect(){
return rect;
}
void dismiss(){
//Remove the actionsPopup from the parent.
if(parent!=NULL && parent->actionsPopup==this){
parent->actionsPopup=NULL;
}
//And delete ourself.
delete this;
}
SharedTexture createItem(SDL_Renderer& renderer,const char* caption,int icon,bool grayed=false){
//FIXME: Add some sort of caching?
//We draw using surfaces and convert to a texture in the end for now.
SDL_Color color = objThemes.getTextColor(true);
if (grayed) {
color.r = 128 + color.r / 2;
color.g = 128 + color.g / 2;
color.b = 128 + color.b / 2;
}
SurfacePtr tip(TTF_RenderUTF8_Blended(fontText,caption,color));
SDL_SetSurfaceBlendMode(tip.get(), SDL_BLENDMODE_NONE);
//Create the surface, we add 16px to the width for an icon,
//plus 8px for the border to make it looks better.
SurfacePtr item = createSurface(tip->w + 24 + (icon >= 0x100 ? 24 : 0), 24);
//Draw the text on the item surface.
applySurface(24 + (icon >= 0x100 ? 24 : 0), 0, tip.get(), item.get(), NULL);
//Temporarily set the blend mode of bmGUI to none, which simply copies RGBA channels to destination.
SDL_BlendMode oldBlendMode = SDL_BLENDMODE_BLEND;
SDL_GetSurfaceBlendMode(bmGUI, &oldBlendMode);
SDL_SetSurfaceBlendMode(bmGUI, SDL_BLENDMODE_NONE);
//Check if we should draw an icon.
if(icon>0){
//Draw the check (or not).
SDL_Rect r={0,0,16,16};
r.x=((icon-1)%8)*16;
r.y=(((icon-1)/8)%8)*16;
applySurface(4,3,bmGUI,item.get(),&r);
}
//Check if we should draw a secondary icon.
if (icon >= 0x100) {
SDL_Rect r = { 0, 0, 16, 16 };
r.x = ((icon / 0x100 - 1) % 8) * 16;
r.y = (((icon / 0x100 - 1) / 8) % 8) * 16;
applySurface(28, 3, bmGUI, item.get(), &r);
}
//Reset the blend mode of bmGUI.
SDL_SetSurfaceBlendMode(bmGUI, oldBlendMode);
//Check if we should update the width., 8px extra on the width is for four pixels spacing on either side.
if(item->w+8>rect.w) {
rect.w=item->w+8;
}
return textureFromSurface(renderer, std::move(item));
}
void updateListBoxSize() {
//Update the size of the GUIListBox.
actions->width = rect.w;
actions->height = rect.h;
int x = rect.x, y = rect.y;
if (x>SCREEN_WIDTH - rect.w) x = SCREEN_WIDTH - rect.w;
else if (x<0) x = 0;
if (y>SCREEN_HEIGHT - rect.h) y = SCREEN_HEIGHT - rect.h;
else if (y<0) y = 0;
rect.x = x;
rect.y = y;
}
void updateItem(SDL_Renderer& renderer,int index,const char* action,const char* caption,int icon=0,bool grayed=false){
auto item=createItem(renderer,caption,icon,grayed);
actions->updateItem(renderer, index,action,item,!grayed);
//Update the size of the GUIListBox.
updateListBoxSize();
}
void addItem(SDL_Renderer& renderer,const char* action,const char* caption,int icon=0,bool grayed=false){
auto item=createItem(renderer,caption,icon,grayed);
actions->addItem(renderer,action,item,!grayed);
//Update the height.
rect.h += 24;
//Update the size of the GUIListBox.
updateListBoxSize();
}
void addSeparator(SDL_Renderer& renderer) {
actions->addItem(renderer, "-", getOrCreateSeparatorTexture(renderer), false);
//Update the height.
rect.h += 5;
}
LevelEditorActionsPopup(ImageManager& imageManager,SDL_Renderer& renderer,LevelEditor* parent, GameObject* target, int x=0, int y=0){
this->parent=parent;
this->target=target;
rect.x = x;
rect.y = y;
//NOTE: The size gets set in the addItem method, height is already four to prevent a scrollbar.
rect.w=0;
rect.h=4;
//Load the gui images.
bmGUI=imageManager.loadImage(getDataPath()+"gfx/gui.png");
//Create the behaviour vector.
behaviour.push_back(_("On"));
behaviour.push_back(_("Off"));
behaviour.push_back(_("Toggle"));
//Create the states list.
states.push_back(_("Complete"));
states.push_back(_("One step"));
states.push_back(_("Two steps"));
states.push_back(_("Gone"));
//TODO: The width should be based on the longest option.
//Create default actions.
//NOTE: Width and height are determined later on when the options are rendered.
actions=new GUIListBox(imageManager,renderer,0,0,0,0);
actions->eventCallback=this;
//Check if it's a block or not.
if(target!=NULL)
addBlockItems(renderer);
else
addLevelItems(renderer);
}
static std::string getRepeatModeName(int mode) {
switch (mode) {
case Scenery::NEGATIVE_INFINITY:
return _("Negative infinity");
case Scenery::ZERO:
return _("Zero");
case Scenery::LEVEL_SIZE:
return _("Level size");
case Scenery::POSITIVE_INFINITY:
return _("Positive infinity");
default:
return _("Default");
}
}
void addBlockItems(SDL_Renderer& renderer){
//Check if the block is selected or not.
std::vector<GameObject*>::iterator it;
it=find(parent->selection.begin(),parent->selection.end(),target);
if(it!=parent->selection.end())
addItem(renderer,"Deselect",_("Deselect"));
else
addItem(renderer,"Select",_("Select"));
addItem(renderer,"Delete",_("Delete"),8);
addSeparator(renderer);
Scenery *scenery = dynamic_cast<Scenery*>(target);
if (scenery) {
// it is scenery block
addItem(renderer, "RepeatMode0", tfm::format(_("Horizontal repeat start: %s"),
getRepeatModeName(scenery->repeatMode & 0xFF)).c_str(), 8 * 2 + 3);
addItem(renderer, "RepeatMode1", tfm::format(_("Horizontal repeat end: %s"),
getRepeatModeName((scenery->repeatMode >> 8) & 0xFF)).c_str(), 8 * 2 + 4);
addItem(renderer, "RepeatMode2", tfm::format(_("Vertical repeat start: %s"),
getRepeatModeName((scenery->repeatMode >> 16) & 0xFF)).c_str(), 8 * 3 + 3);
addItem(renderer, "RepeatMode3", tfm::format(_("Vertical repeat end: %s"),
getRepeatModeName((scenery->repeatMode >> 24) & 0xFF)).c_str(), 8 * 3 + 4);
if (scenery->sceneryName_.empty()) {
addSeparator(renderer);
addItem(renderer, "CustomScenery", _("Custom scenery"), 8 + 4);
}
return;
}
addItem(renderer, "Visible", _("Visible"), (target->getEditorProperty("visible") == "1") ? 2 : 1);
//Get the type of the target.
int type = target->type;
//Determine what to do depending on the type.
if(isLinkable[type]){
//Check if it's a moving block type or trigger.
if(type==TYPE_BUTTON || type==TYPE_SWITCH || type==TYPE_PORTAL){
addItem(renderer, "Link", _("Link"), 8 * 2 + 8);
addItem(renderer, "Remove Links", _("Remove Links"), 8 * 2 + 7);
//Check if it's a portal, which contains a automatic option, and triggers a behaviour one.
if(type==TYPE_PORTAL){
addItem(renderer,"Automatic",_("Automatic"),(target->getEditorProperty("automatic")=="1")?2:1);
}else{
//Get the current behaviour.
int currentBehaviour=2;
if(target->getEditorProperty("behaviour")=="on"){
currentBehaviour=0;
}else if(target->getEditorProperty("behaviour")=="off"){
currentBehaviour=1;
}
addItem(renderer, "Behaviour", tfm::format(_("Behavior: %s"), behaviour[currentBehaviour]).c_str());
}
}else{
addItem(renderer, "Path", _("Path"), 8 + 5);
addItem(renderer, "Remove Path", _("Remove Path"), 8 * 4 + 2);
addItem(renderer,"Activated",_("Activated"),(target->getEditorProperty("activated")=="1")?2:1);
addItem(renderer,"Looping",_("Looping"),(target->getEditorProperty("loop")=="1")?2:1);
}
}
//Check for a conveyor belt.
if(type==TYPE_CONVEYOR_BELT || type==TYPE_SHADOW_CONVEYOR_BELT){
addItem(renderer,"Activated",_("Activated"),(target->getEditorProperty("activated")=="1")?2:1);
addItem(renderer,"Speed",_("Speed"));
}
//Check if it's a fragile block.
if(type==TYPE_FRAGILE){
//Get the current state.
int currentState=atoi(target->getEditorProperty("state").c_str());
addItem(renderer, "State", tfm::format(_("State: %s"), states[currentState]).c_str());
}
//Check if it's a notification block.
if (type == TYPE_NOTIFICATION_BLOCK)
addItem(renderer, "Message", _("Message"), 8 * 4 + 5);
else if (type == TYPE_PORTAL || type == TYPE_SWITCH) {
char s[] = {
'M', 'e', 's', 's', 'a', 'g', 'e', '2', (char)type, '\0',
};
addItem(renderer, s, _("Message"), 8 * 4 + 5);
}
//Add the custom appearance menu item.
addItem(renderer, "Appearance", _("Appearance"), 8 + 4);
addSeparator(renderer);
//Finally add scripting to the bottom.
addItem(renderer,"Scripting",_("Scripting"),8*2+1);
}
void addLevelItems(SDL_Renderer& renderer){
// add the layers
{
// background layers.
auto it = parent->sceneryLayers.begin();
for (; it != parent->sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
int icon = parent->layerVisibility[it->first] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer == it->first ? 3 : 36) << 8;
std::string s = "_layer:" + it->first;
addItem(renderer, s.c_str(), tfm::format(_("Background layer: %s"), it->first.substr(3)).c_str(), icon);
}
// the Blocks layer.
{
int icon = parent->layerVisibility[std::string()] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer.empty() ? 3 : 36) << 8;
addItem(renderer, "_layer:", _("Blocks layer"), icon);
}
// foreground layers.
for (; it != parent->sceneryLayers.end(); ++it){
int icon = parent->layerVisibility[it->first] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer == it->first ? 3 : 36) << 8;
std::string s = "_layer:" + it->first;
addItem(renderer, s.c_str(), tfm::format(_("Foreground layer: %s"), it->first.substr(3)).c_str(), icon);
}
}
addSeparator(renderer);
addItem(renderer, "AddLayer", _("Add new layer"), 8 * 3 + 6);
addItem(renderer, "DeleteLayer", _("Delete selected layer"), 8 * 3 + 7, parent->selectedLayer.empty());
addItem(renderer, "LayerSettings", _("Configure selected layer"), 8 * 3 + 8, parent->selectedLayer.empty());
addItem(renderer, "MoveToLayer", _("Move selected object to layer"), 0, parent->selectedLayer.empty() || parent->selection.empty());
addSeparator(renderer);
addItem(renderer,"LevelSettings",_("Settings"),8*2);
addItem(renderer,"LevelScripting",_("Scripting"),8*2+1);
}
virtual ~LevelEditorActionsPopup(){
//bmGui is freed by imageManager.
if(actions)
delete actions;
}
void render(SDL_Renderer& renderer){
//Check if we need to resize the separator.
//NOTE: if separatorTexture is NULL then it means that we didn't use the separator at all, so we don't need to update it.
if (separatorTexture && textureWidth(*separatorTexture) < rect.w) {
updateSeparators(renderer);
}
//Draw the actions.
actions->render(renderer,rect.x,rect.y);
}
void handleEvents(SDL_Renderer& renderer){
//Check if a mouse is pressed outside the popup.
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
if(event.type==SDL_MOUSEBUTTONDOWN && !pointOnRect(mouse,rect)){
dismiss();
return;
}
//Let the listbox handle its events.
actions->handleEvents(renderer,rect.x,rect.y);
}
static void addLayerNameNote(ImageManager& imageManager, SDL_Renderer& renderer, GUIWindow *root, int yy = 148) {
std::string s = _("NOTE: the layers are sorted by name alphabetically.");
for (int lps = 0;;) {
size_t lpe = s.find_first_of('\n', lps);
GUIObject *obj = new GUILabel(imageManager, renderer, 40, yy, 520, 36,
lpe == string::npos ? (s.c_str() + lps) : s.substr(lps, lpe - lps).c_str());
root->addChild(obj);
if (lpe == string::npos) break;
lps = lpe + 1;
yy += 24;
}
}
void GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Skip next mouse up event since we're clicking a list box and possibly showing a new window.
GUISkipNextMouseUpEvent = true;
//NOTE: There should only be one GUIObject, so we know what event is fired.
//Get the selected entry.
std::string action=actions->item[actions->value];
if(action=="Select"){
//Add the target to the selection.
parent->selection.push_back(target);
dismiss();
return;
}else if(action=="Deselect"){
//Check if the block is in the selection.
std::vector<GameObject*>::iterator it;
it=find(parent->selection.begin(),parent->selection.end(),target);
if(it!=parent->selection.end()){
//Remove the object from the selection.
parent->selection.erase(it);
}
dismiss();
return;
}else if(action=="Delete"){
parent->commandManager->doCommand(new AddRemoveGameObjectCommand(parent, target, false));
dismiss();
return;
}else if(action=="Link"){
parent->linking=true;
parent->linkingTrigger=dynamic_cast<Block*>(target);
parent->tool=LevelEditor::SELECT;
dismiss();
return;
}else if(action=="Remove Links"){
//Remove all the links
Block *block = dynamic_cast<Block*>(target);
if (block) {
RemoveLinkCommand* pCommand = new RemoveLinkCommand(parent, block);
parent->commandManager->doCommand(pCommand);
}
dismiss();
return;
}else if(action=="Path"){
parent->moving=true;
parent->pauseMode = false;
parent->pauseTime = 0;
parent->movingBlock=dynamic_cast<Block*>(target);
parent->tool=LevelEditor::SELECT;
dismiss();
return;
}else if(action=="Remove Path"){
//Remove all the paths
Block *block = dynamic_cast<Block*>(target);
if (block) {
RemovePathCommand* pCommand = new RemovePathCommand(parent, block);
parent->commandManager->doCommand(pCommand);
}
dismiss();
return;
}else if(action=="Message"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-250)/2,600,250,true,true,_("Notification block"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="notificationBlockWindow";
root->eventCallback=parent;
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,40,50,240,36,_("Enter message here:"));
root->addChild(obj);
GUITextArea* textarea=new GUITextArea(imageManager,renderer,50,90,500,100);
textarea->gravityRight = textarea->gravityBottom = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
textarea->name="message";
- string tmp=target->getEditorProperty("message");
+
//Change \n with the characters '\n'.
- while(tmp.find("\\n")!=string::npos){
- tmp=tmp.replace(tmp.find("\\n"),2,"\n");
- }
+ string tmp = unescapeNewline(target->getEditorProperty("message"));
textarea->setString(renderer, tmp);
root->addChild(textarea);
obj=new GUIButton(imageManager,renderer,root->width*0.3,250-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgNotificationBlockOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,250-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
} else if (action.size() >= 9 && action.substr(0, 8) == "Message2") {
int type = (int)(unsigned char)action[8];
//Create the GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 250) / 2, 600, 250, true, true, _(LevelEditor::blockNames[type]));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "notificationBlockWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 40, 50, 240, 36, _("Enter message here:"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 50, 100, 500, 36, "Press %s key to");
root->addChild(obj);
int w = 0;
TTF_SizeUTF8(fontText, "Press %s key to", &w, NULL);
obj = new GUITextBox(imageManager, renderer, 60 + w, 100, 490 - w, 36);
obj->name = "message";
obj->caption = target->getEditorProperty("message");
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 150, 240, 36, _("Example:"));
root->addChild(obj);
std::string s;
switch (type) {
case TYPE_PORTAL:
s = "teleport";
break;
case TYPE_SWITCH:
s = "activate the switch";
break;
}
obj = new GUILabel(imageManager, renderer, 60 + w, 150, 490 - w, 36, s.c_str());
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 250 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgNotificationBlockOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 250 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action == "Activated"){
//Get the previous state.
bool enabled=(target->getEditorProperty("activated")=="1");
//Switch the state.
enabled=!enabled;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "activated", enabled ? "1" : "0", _("Activated")));
updateItem(renderer,actions->value,"Activated",_("Activated"),enabled?2:1);
actions->value=-1;
return;
} else if (action == "Visible"){
//Get the previous state.
bool visible = (target->getEditorProperty("visible") == "1");
//Switch the state.
visible = !visible;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "visible", visible ? "1" : "0", _("Visible")));
updateItem(renderer, actions->value, "Visible", _("Visible"), visible ? 2 : 1);
actions->value = -1;
return;
} else if (action == "Looping"){
//Get the previous state.
bool loop=(target->getEditorProperty("loop")=="1");
//Switch the state.
loop=!loop;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "loop", loop ? "1" : "0", _("Looping")));
updateItem(renderer,actions->value,"Looping",_("Looping"),loop?2:1);
actions->value=-1;
return;
}else if(action=="Automatic"){
//Get the previous state.
bool automatic=(target->getEditorProperty("automatic")=="1");
//Switch the state.
automatic=!automatic;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "automatic", automatic ? "1" : "0", _("Automatic")));
updateItem(renderer,actions->value,"Automatic",_("Automatic"),automatic?2:1);
actions->value=-1;
return;
}else if(action=="Behaviour"){
//Get the current behaviour.
int currentBehaviour=2;
string behave=target->getEditorProperty("behaviour");
if(behave=="on"){
currentBehaviour=0;
}else if(behave=="off"){
currentBehaviour=1;
}
//Increase the behaviour.
currentBehaviour++;
if(currentBehaviour>2)
currentBehaviour=0;
//Update the data of the block.
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "behaviour", behaviour[currentBehaviour], _("Behavior")));
//And update the item.
updateItem(renderer, actions->value, "Behaviour", tfm::format(_("Behavior: %s"), behaviour[currentBehaviour]).c_str());
actions->value=-1;
return;
}else if(action=="State"){
//Get the current state.
int currentState=atoi(target->getEditorProperty("state").c_str());
//Increase the state.
currentState++;
if(currentState>3)
currentState=0;
//Update the data of the block.
char s[64];
sprintf(s,"%d",currentState);
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "state", s, _("State")));
//And update the item.
updateItem(renderer, actions->value, "State", tfm::format(_("State: %s"), states[currentState]).c_str());
actions->value=-1;
return;
}else if(action=="Speed"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-250)/2,600,250,true,true,_("Conveyor belt speed"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="conveyorBlockWindow";
root->eventCallback=parent;
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,40,100,240,36,_("Enter speed here:"));
root->addChild(obj);
GUISpinBox* obj2=new GUISpinBox(imageManager,renderer,240,100,320,36);
obj2->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
obj2->name="speed";
obj2->caption=target->getEditorProperty("speed10");
obj2->format = "%1.0f";
obj2->update();
root->addChild(obj2);
obj = new GUILabel(imageManager, renderer, 40, 160, 520, 36, _("NOTE: 1 Speed = 0.08 block/s"));
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.3,250-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgConveyorBlockOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,250-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
}else if(action=="Scripting"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-500)/2,600,500,true,true,_("Scripting"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="scriptingWindow";
root->eventCallback=parent;
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,50,60,240,36,_("Id:"));
root->addChild(obj);
obj=new GUITextBox(imageManager,renderer,100,60,240,36,dynamic_cast<Block*>(target)->id.c_str());
obj->gravityRight = GUIGravityRight;
obj->name="id";
root->addChild(obj);
GUISingleLineListBox* list=new GUISingleLineListBox(imageManager,renderer,50,100,500,36);
list->gravityRight = GUIGravityRight;
std::map<std::string,int>::iterator it;
for(it=Game::gameObjectEventNameMap.begin();it!=Game::gameObjectEventNameMap.end();++it)
list->addItem(it->first);
list->name="cfgScriptingEventType";
list->value=0;
list->eventCallback=root;
root->addChild(list);
//Add a text area for each event type.
Block* block=dynamic_cast<Block*>(target);
for(unsigned int i=0;i<list->item.size();i++){
GUITextArea* text=new GUITextArea(imageManager,renderer,50,140,500,300);
text->gravityRight = text->gravityBottom = GUIGravityRight;
text->name=list->item[i].first;
text->setFont(fontMono);
//Only set the first one visible and enabled.
text->visible=(i==0);
text->enabled=(i==0);
map<int,string>::iterator it=block->scripts.find(Game::gameObjectEventNameMap[list->item[i].first]);
if(it!=block->scripts.end())
text->setString(renderer, it->second);
root->addChild(text);
}
obj=new GUIButton(imageManager,renderer,root->width*0.3,500-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgScriptingOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,500-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
}else if(action=="LevelSettings"){
//Open the levelSettings window.
parent->levelSettings(imageManager,renderer);
//And dismiss this popup.
dismiss();
return;
}else if(action=="LevelScripting"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-500)/2,600,500,true,true,_("Level Scripting"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="levelScriptingWindow";
root->eventCallback=parent;
GUIObject* obj;
GUISingleLineListBox* list=new GUISingleLineListBox(imageManager,renderer,50,60,500,36);
list->gravityRight = GUIGravityRight;
std::map<std::string,int>::iterator it;
for(it=Game::levelEventNameMap.begin();it!=Game::levelEventNameMap.end();++it)
list->addItem(it->first);
list->name="cfgLevelScriptingEventType";
list->value=0;
list->eventCallback=root;
root->addChild(list);
//Add a text area for each event type.
for(unsigned int i=0;i<list->item.size();i++){
GUITextArea* text=new GUITextArea(imageManager,renderer,50,100,500,340);
text->gravityRight = text->gravityBottom = GUIGravityRight;
text->name=list->item[i].first;
text->setFont(fontMono);
//Only set the first one visible and enabled.
text->visible=(i==0);
text->enabled=(i==0);
map<int,string>::iterator it=parent->scripts.find(Game::levelEventNameMap[list->item[i].first]);
if(it!=parent->scripts.end())
text->setString(renderer, it->second);
root->addChild(text);
}
obj=new GUIButton(imageManager,renderer,root->width*0.3,500-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgLevelScriptingOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,500-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
} else if (action.size() >= 7 && action.substr(0, 7) == "_layer:") {
std::string s0 = action.substr(7);
auto it = parent->layerVisibility.find(s0);
if (it != parent->layerVisibility.end()) {
int x;
SDL_GetMouseState(&x, NULL);
if (x < rect.x + 24) {
// toggle the visibility
it->second = !it->second;
if (parent->selectedLayer == it->first) {
// deselect all blocks if the visibility of current layer is changed
parent->deselectAll();
}
} else if (parent->selectedLayer != it->first) {
// deselect all blocks if the selected layer is changed
parent->deselectAll();
// uncheck the previously selected layer
std::string oldSelected = "_layer:" + parent->selectedLayer;
for (unsigned int idx = 0; idx < actions->item.size(); idx++) {
if (actions->item[idx] == oldSelected) {
int icon = parent->layerVisibility[parent->selectedLayer] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= 36 << 8;
updateItem(renderer, idx, oldSelected.c_str(),
parent->selectedLayer.empty() ? _("Blocks layer") :
tfm::format((parent->selectedLayer < "f") ? _("Background layer: %s") : _("Foreground layer: %s"), parent->selectedLayer.substr(3)).c_str(),
icon);
break;
}
}
// change the selected layer
parent->selectedLayer = it->first;
} else {
actions->value = -1;
return;
}
int icon = it->second ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer == it->first ? 3 : 36) << 8;
std::string s = "_layer:" + it->first;
updateItem(renderer, actions->value, s.c_str(),
it->first.empty() ? _("Blocks layer") :
tfm::format((it->first < "f") ? _("Background layer: %s") : _("Foreground layer: %s"), it->first.substr(3)).c_str(),
icon);
// update some other menu items according to selection/visibility changes
for (unsigned int i = 0; i < actions->item.size(); i++) {
if (actions->item[i] == "DeleteLayer") {
updateItem(renderer, i, "DeleteLayer", _("Delete selected layer"), 8 * 3 + 7, parent->selectedLayer.empty());
} else if (actions->item[i] == "LayerSettings") {
updateItem(renderer, i, "LayerSettings", _("Configure selected layer"), 8 * 3 + 8, parent->selectedLayer.empty());
} else if (actions->item[i] == "MoveToLayer") {
updateItem(renderer, i, "MoveToLayer", _("Move selected object to layer"), 0, parent->selectedLayer.empty() || parent->selection.empty());
}
}
}
actions->value = -1;
return;
} else if (action == "AddLayer") {
//Create the add layer GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 350) / 2, 600, 350, true, true, _("Add layer"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "addLayerWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 40, 64, 520, 36, _("Enter the layer name:"));
root->addChild(obj);
GUITextBox* obj2 = new GUITextBox(imageManager, renderer, 40, 100, 520, 36);
obj2->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
obj2->name = "layerName";
root->addChild(obj2);
addLayerNameNote(imageManager, renderer, root);
obj = new GUILabel(imageManager, renderer, 40, 185, 220, 50, _("Layer type:"));
root->addChild(obj);
GUISingleLineListBox *obj3 = new GUISingleLineListBox(imageManager, renderer, 260, 185, 300, 50);
obj3->name = "layerType";
obj3->addItem(_("Background layer"));
obj3->addItem(_("Foreground layer"));
obj3->value = 0;
root->addChild(obj3);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 350 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgAddLayerOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 350 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action == "DeleteLayer") {
// delete selected layer
if (parent->selectedLayer.empty()) {
// can't delete Blocks layer
actions->value = -1;
return;
}
if (parent->sceneryLayers.find(parent->selectedLayer) == parent->sceneryLayers.end()) {
// can't find the layer with given name
actions->value = -1;
return;
}
if (msgBox(imageManager, renderer,
tfm::format(
(parent->selectedLayer < "f") ? _("Are you sure you want to delete background layer '%s'?") : _("Are you sure you want to delete foreground layer '%s'?"),
parent->selectedLayer.substr(3)).c_str(),
MsgBoxYesNo, _("Delete layer")) == MsgBoxYes)
{
// do the actual operation
parent->commandManager->doCommand(new AddRemoveLayerCommand(parent, parent->selectedLayer, false));
}
dismiss();
return;
} else if (action == "LayerSettings") {
// rename selected layer
if (parent->selectedLayer.empty()) {
// can't rename Blocks layer
actions->value = -1;
return;
}
auto it = parent->sceneryLayers.find(parent->selectedLayer);
if (it == parent->sceneryLayers.end()) {
// can't find the layer with given name
actions->value = -1;
return;
}
//Create the rename layer GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 450) / 2, 600, 450, true, true, _("Layer settings"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "layerSettingsWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 40, 64, 520, 36, _("Layer name:"));
root->addChild(obj);
GUITextBox* textBox = new GUITextBox(imageManager, renderer, 40, 100, 520, 36, it->first.c_str() + 3);
textBox->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
textBox->name = "layerName";
root->addChild(textBox);
// A stupid code to save the old name
obj = new GUIObject(imageManager, renderer, 0, 0, 0, 0, it->first.c_str(), 0, false, false);
obj->name = "oldName";
root->addChild(obj);
addLayerNameNote(imageManager, renderer, root);
obj = new GUILabel(imageManager, renderer, 40, 185, 220, 50, _("Layer type:"));
root->addChild(obj);
GUISingleLineListBox *obj3 = new GUISingleLineListBox(imageManager, renderer, 260, 185, 300, 50);
obj3->name = "layerType";
obj3->addItem(_("Background layer"));
obj3->addItem(_("Foreground layer"));
obj3->value = it->first < "f" ? 0 : 1;
root->addChild(obj3);
obj = new GUILabel(imageManager, renderer, 40, 234, 520, 36, _("Layer moving speed (1 speed = 0.8 block/s):"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 270, 40, 36, "X");
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 320, 270, 40, 36, "Y");
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
root->addChild(obj);
char s[128];
GUISpinBox *spinBox = new GUISpinBox(imageManager, renderer, 80, 270, 200, 36);
spinBox->gravityRight = GUIGravityCenter;
spinBox->name = "speedX";
sprintf(s, "%g", it->second->speedX);
spinBox->caption = s;
spinBox->format = "%g";
root->addChild(spinBox);
spinBox = new GUISpinBox(imageManager, renderer, 360, 270, 200, 36);
spinBox->gravityLeft = GUIGravityCenter; spinBox->gravityRight = GUIGravityRight;
spinBox->name = "speedY";
sprintf(s, "%g", it->second->speedY);
spinBox->caption = s;
spinBox->format = "%g";
root->addChild(spinBox);
obj = new GUILabel(imageManager, renderer, 40, 314, 520, 36, _("Speed of following camera:"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 350, 40, 36, "X");
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 320, 350, 40, 36, "Y");
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
root->addChild(obj);
spinBox = new GUISpinBox(imageManager, renderer, 80, 350, 200, 36);
spinBox->gravityRight = GUIGravityCenter;
spinBox->name = "cameraX";
sprintf(s, "%g", it->second->cameraX);
spinBox->caption = s;
spinBox->format = "%g";
spinBox->change = 0.1f;
root->addChild(spinBox);
spinBox = new GUISpinBox(imageManager, renderer, 360, 350, 200, 36);
spinBox->gravityLeft = GUIGravityCenter; spinBox->gravityRight = GUIGravityRight;
spinBox->name = "cameraY";
sprintf(s, "%g", it->second->cameraY);
spinBox->caption = s;
spinBox->format = "%g";
spinBox->change = 0.1f;
root->addChild(spinBox);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 450 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgLayerSettingsOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 450 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action == "MoveToLayer") {
// move the selected object to another layer
if (parent->selection.empty() || parent->selectedLayer.empty()) {
// either nothing selected, or can't move objects in Blocks layer
actions->value = -1;
return;
}
//Create the rename layer GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 350) / 2, 600, 350, true, true, _("Move to layer"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "moveToLayerWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 40, 64, 520, 36, _("Enter the layer name (create new layer if necessary):"));
root->addChild(obj);
GUITextBox* obj2 = new GUITextBox(imageManager, renderer, 40, 100, 520, 36, parent->selectedLayer.c_str() + 3);
obj2->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
obj2->name = "layerName";
root->addChild(obj2);
// A stupid code to save the old name
obj = new GUIObject(imageManager, renderer, 0, 0, 0, 0, parent->selectedLayer.c_str(), 0, false, false);
obj->name = "oldName";
root->addChild(obj);
addLayerNameNote(imageManager, renderer, root);
obj = new GUILabel(imageManager, renderer, 40, 185, 220, 50, _("Layer type:"));
root->addChild(obj);
GUISingleLineListBox *obj3 = new GUISingleLineListBox(imageManager, renderer, 260, 185, 300, 50);
obj3->name = "layerType";
obj3->addItem(_("Background layer"));
obj3->addItem(_("Foreground layer"));
obj3->value = parent->selectedLayer < "f" ? 0 : 1;
root->addChild(obj3);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 350 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgMoveToLayerOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 350 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action.size() > 10 && action.substr(0, 10) == "RepeatMode") {
Scenery *scenery = dynamic_cast<Scenery*>(target);
if (scenery) {
int index = atoi(action.c_str() + 10);
assert(index >= 0 && index < 4);
//Get the current repeat mode.
unsigned int repeatMode = scenery->repeatMode;
//Extract the value we want to modify.
unsigned int i = (repeatMode >> (index * 8)) & 0xFF;
repeatMode &= ~(0xFFu << (index * 8));
//Increase the value.
for (;;) {
i++;
if (i >= Scenery::REPEAT_MODE_MAX) i = 0;
// skip invalid values (POSITIVE_INFINITY for start, NEGATIVE_INFINITY for end)
if ((index & 1) == 0 && i == Scenery::POSITIVE_INFINITY) continue;
if ((index & 1) == 1 && i == Scenery::NEGATIVE_INFINITY) continue;
break;
}
//Update the repeat mode of the block.
char s[64];
sprintf(s, "%u", repeatMode | (i << (index * 8)));
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "repeatMode", s, _("Repeat mode")));
//And update the item.
const char* ss[4] = {
__("Horizontal repeat start: %s"),
__("Horizontal repeat end: %s"),
__("Vertical repeat start: %s"),
__("Vertical repeat end: %s"),
};
const int icons[4] = {
8 * 2 + 3, 8 * 2 + 4, 8 * 3 + 3, 8 * 3 + 4,
};
updateItem(renderer, actions->value, action.c_str(), tfm::format(_(ss[index]), getRepeatModeName(i)).c_str(), icons[index]);
}
actions->value = -1;
return;
} else if (action == "CustomScenery") {
//Create the GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 400) / 2, 600, 400, true, true, _("Custom scenery"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "customSceneryWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 50, 60, 240, 36, _("Custom scenery:"));
root->addChild(obj);
//Add a text area.
Scenery* scenery = dynamic_cast<Scenery*>(target);
GUITextArea* text = new GUITextArea(imageManager, renderer, 50, 100, 500, 240);
text->gravityRight = text->gravityBottom = GUIGravityRight;
text->name = "cfgCustomScenery";
text->setFont(fontMono);
//Only set the first one visible and enabled.
text->visible = true;
text->enabled = true;
// FIXME: an ad-hoc code
std::string s;
for (char c : scenery->customScenery_) {
if (c == '\t')
s.append(" ");
else
s.push_back(c);
}
text->setString(renderer, s);
root->addChild(text);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 400 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCustomSceneryOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 400 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action == "Appearance"){
//Create the GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 400) / 2, 600, 400, true, true, _("Appearance"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "appearanceWindow";
root->eventCallback = parent;
GUIObject* obj;
Block* block = dynamic_cast<Block*>(target);
//Create a list box showing all available scenery blocks.
GUIListBox *list = new GUIListBox(imageManager, renderer, 50, 60, 440, 280);
list->gravityRight = list->gravityBottom = GUIGravityRight;
list->name = "lstAppearance";
list->eventCallback = root;
root->addChild(list);
//TODO: Show the image along with the text in the list box.
//Currently I don't like to do that because this requires a lot of video memory since there are a lot of available scenery blocks.
list->addItem(renderer, _("(Use the default appearance for this block)"));
if (block->customAppearanceName.empty()) list->value = 0;
for (const std::string& s : parent->sceneryBlockNames) {
list->addItem(renderer, LevelEditor::describeSceneryName(s));
if (block->customAppearanceName == s) list->value = list->item.size() - 1;
}
if (list->value < 0) {
std::cerr << "WARNING: The custom appearance '" << block->customAppearanceName << "' is unrecognized, added it to the list box anyway" << std::endl;
list->addItem(renderer, block->customAppearanceName);
list->value = list->item.size() - 1;
}
//Ask the list box to update scrollbar and we scroll the scrollbar to the correct position (FIXME: ad-hoc code)
list->render(renderer, 0, 0, false);
list->scrollScrollbar(list->value);
//Create an image widget showing the appearance of selected scenery block.
GUIImage *image = new GUIImage(imageManager, renderer, 500, 60, 50, 50);
image->gravityLeft = image->gravityRight = GUIGravityRight;
image->name = "imgAppearance";
root->addChild(image);
//Add a Change event to the list box which will be processed next frame, which is used to update the image widget.
GUIEventQueue.push_back(GUIEvent{ list->eventCallback, list->name, list, GUIEventChange });
obj = new GUIButton(imageManager, renderer, root->width*0.3, 400 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgAppearanceOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 400 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
}
}
};
/////////////////LevelEditorSelectionPopup/////////////////
class LevelEditorSelectionPopup{
private:
//The parent object.
LevelEditor* parent;
//The position of window.
SDL_Rect rect;
//GUI image.
SharedTexture bmGUI;
//The selection
std::vector<GameObject*> selection;
//The scrollbar.
GUIScrollBar* scrollBar;
//Highlighted object.
GameObject* highlightedObj;
//Highlighted button index. 0=none 1=select/deselect 2=delete 3=configure
int highlightedBtn;
public:
int startRow,showedRow;
//If selection is dirty
bool dirty;
public:
SDL_Rect getRect(){
return rect;
}
int width(){
return rect.w;
}
int height(){
return rect.h;
}
void updateScrollBar(ImageManager& imageManager, SDL_Renderer& renderer){
int m=selection.size()-showedRow;
if(m>0){
if(startRow<0) startRow=0;
else if(startRow>m) startRow=m;
if(scrollBar==NULL){
scrollBar=new GUIScrollBar(imageManager,renderer,0,0,16,rect.h-16,ScrollBarVertical,startRow,0,m,1,showedRow);
}
scrollBar->visible=true;
scrollBar->maxValue=m;
scrollBar->value=startRow;
}else{
startRow=0;
if(scrollBar){
scrollBar->visible=false;
scrollBar->value=0;
}
}
}
void updateSelection(ImageManager& imageManager, SDL_Renderer& renderer){
if(parent!=NULL){
std::vector<Block*>& v=parent->levelObjects;
for(int i=selection.size()-1;i>=0;i--){
if(find(v.begin(),v.end(),selection[i])==v.end()){
selection.erase(selection.begin()+i);
}
}
updateScrollBar(imageManager,renderer);
}
}
void dismiss(){
if(parent!=NULL && parent->selectionPopup==this){
parent->selectionPopup=NULL;
}
delete this;
}
LevelEditorSelectionPopup(LevelEditor* parent, ImageManager& imageManager, SDL_Renderer& renderer, std::vector<GameObject*>& selection, int x=0, int y=0){
this->parent=parent;
this->selection=selection;
dirty=false;
scrollBar=NULL;
highlightedObj=NULL;
highlightedBtn=0;
//calc window size
startRow=0;
showedRow=selection.size();
int m=SCREEN_HEIGHT/64-1;
if(showedRow>m) showedRow=m;
rect.w=320;
rect.h=showedRow*64+16;
if(x>SCREEN_WIDTH-rect.w) x=SCREEN_WIDTH-rect.w;
else if(x<0) x=0;
if(y>SCREEN_HEIGHT-rect.h) y=SCREEN_HEIGHT-rect.h;
else if(y<0) y=0;
rect.x=x;
rect.y=y;
updateScrollBar(imageManager,renderer);
//Load the gui images.
bmGUI=imageManager.loadTexture(getDataPath()+"gfx/gui.png",renderer);
}
virtual ~LevelEditorSelectionPopup(){
if(scrollBar)
delete scrollBar;
}
void move(int x,int y){
if(x>SCREEN_WIDTH-rect.w) x=SCREEN_WIDTH-rect.w;
else if(x<0) x=0;
if(y>SCREEN_HEIGHT-rect.h) y=SCREEN_HEIGHT-rect.h;
else if(y<0) y=0;
rect.x=x;
rect.y=y;
}
void render(ImageManager& imageManager, SDL_Renderer& renderer){
//Check dirty
if(dirty){
updateSelection(imageManager,renderer);
if(selection.empty()){
dismiss();
return;
}
dirty=false;
}
//background
drawGUIBox(rect.x,rect.y,rect.w,rect.h,renderer,0xFFFFFFFFU);
//get mouse position
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
//the tool tip of item
SDL_Rect tooltipRect;
//string tooltip;
if(scrollBar && scrollBar->visible){
startRow=scrollBar->value;
}
highlightedObj=NULL;
highlightedBtn=0;
ToolTips toolTip = ToolTips::TooltipMax;
int maxWidth = 0;
//draw avaliable item
for(int i=0;i<showedRow;i++){
int j=startRow+i;
if(j>=(int)selection.size()) break;
SDL_Rect r={rect.x+8,rect.y+i*64+8,rect.w-16,64};
if(scrollBar && scrollBar->visible) r.w-=24;
//check highlight
if(pointOnRect(mouse,r)){
highlightedObj=selection[j];
//0xCCCCCC
SDL_SetRenderDrawColor(&renderer,0xCC,0xCC,0xCC,0xFF);
SDL_RenderFillRect(&renderer,&r);
}
const int type = selection[j]->type;
Scenery *scenery = dynamic_cast<Scenery*>(selection[j]);
if (scenery) {
if (scenery->themeBlock == &(scenery->internalThemeBlock)) {
// custom scenery, draw an ad-hoc stupid icon
if (parent) {
const SDL_Rect srcRect = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { r.x + 7, r.y + 7, 16, 16 };
SDL_RenderCopy(&renderer, parent->bmGUI.get(), &srcRect, &dstRect);
}
} else {
scenery->themeBlock->editorPicture.draw(renderer, r.x + 7, r.y + 7);
}
} else {
//draw tile picture
ThemeBlock* obj = objThemes.getBlock(type);
if (obj){
//obj->editorPicture.draw(screen,r.x+7,r.y+7);
obj->editorPicture.draw(renderer, r.x + 7, r.y + 7);
}
}
if(parent!=NULL){
//draw name
TexturePtr& tex = scenery ? (parent->getCachedTextTexture(renderer, scenery->sceneryName_.empty()
/// TRANSLATORS: Block name
? std::string(_("Custom scenery block")) : LevelEditor::describeSceneryName(scenery->sceneryName_)))
: parent->typeTextTextures.at(type);
if (tex) {
const int w = textureWidth(tex) + 160;
if (w > maxWidth) maxWidth = w;
applyTexture(r.x + 64, r.y + (64 - textureHeight(tex)) / 2, tex, renderer);
}
//draw selected
{
std::vector<GameObject*> &v=parent->selection;
bool isSelected=find(v.begin(),v.end(),selection[j])!=v.end();
SDL_Rect r1={isSelected?16:0,0,16,16};
SDL_Rect r2={r.x+r.w-72,r.y+20,24,24};
if(pointOnRect(mouse,r2)){
drawGUIBox(r2.x,r2.y,r2.w,r2.h,renderer,0x999999FFU);
tooltipRect=r2;
//tooltip=_("Select");
highlightedBtn=1;
toolTip=ToolTips::Select_UsedInSelectionPopup;
}
r2.x+=4;
r2.y+=4;
r2.w=r1.w;
r2.h=r1.h;
SDL_RenderCopy(&renderer, bmGUI.get(),&r1,&r2);
}
//draw delete
{
SDL_Rect r1={112,0,16,16};
SDL_Rect r2={r.x+r.w-48,r.y+20,24,24};
if(pointOnRect(mouse,r2)){
drawGUIBox(r2.x,r2.y,r2.w,r2.h,renderer,0x999999FFU);
tooltipRect=r2;
//tooltip=_("Delete");
highlightedBtn=2;
toolTip=ToolTips::Delete_UsedInSelectionPopup;
}
r2.x+=4;
r2.y+=4;
r2.w=r1.w;
r2.h=r1.h;
SDL_RenderCopy(&renderer, bmGUI.get(),&r1,&r2);
}
//draw configure
{
SDL_Rect r1={112,16,16,16};
SDL_Rect r2={r.x+r.w-24,r.y+20,24,24};
if(pointOnRect(mouse,r2)){
drawGUIBox(r2.x,r2.y,r2.w,r2.h,renderer,0x999999FFU);
tooltipRect=r2;
//tooltip=_("Configure");
toolTip=ToolTips::Configure_UsedInSelectionPopup;
highlightedBtn=3;
}
r2.x+=4;
r2.y+=4;
r2.w=r1.w;
r2.h=r1.h;
SDL_RenderCopy(&renderer, bmGUI.get(),&r1,&r2);
}
}
}
//draw scrollbar
if(scrollBar && scrollBar->visible){
scrollBar->render(renderer,rect.x+rect.w-24,rect.y+8);
}
//draw tooltip
if(parent && int(toolTip) < parent->tooltipTextures.size()){
//Tool specific text.
TexturePtr& tip=parent->tooltipTextures.at(size_t(toolTip));
//Draw only if there's a tooltip available
if(tip){
const auto tipSize = rectFromTexture(tip);
tooltipRect.y-=4;
tooltipRect.h+=8;
if(tooltipRect.y+tooltipRect.h+tipSize.h>SCREEN_HEIGHT-20)
tooltipRect.y-=tipSize.h;
else
tooltipRect.y+=tooltipRect.h;
if(tooltipRect.x+tipSize.w>SCREEN_WIDTH-20)
tooltipRect.x=SCREEN_WIDTH-20-tipSize.w;
//Draw borders around text
Uint32 color=0xFFFFFF00|230;
drawGUIBox(tooltipRect.x-2,tooltipRect.y-2,tipSize.w+4,tipSize.h+4,renderer,color);
//Draw tooltip's text
applyTexture(tooltipRect.x,tooltipRect.y,tip,renderer);
}
}
//Resize the selection popup if necessary
if (maxWidth > rect.w) {
rect.w = maxWidth;
move(rect.x, rect.y);
}
}
void handleEvents(ImageManager& imageManager,SDL_Renderer& renderer){
//Check dirty
if(dirty){
updateSelection(imageManager,renderer);
if(selection.empty()){
dismiss();
return;
}
dirty=false;
}
//Check scrollbar event
if(scrollBar && scrollBar->visible){
if(scrollBar->handleEvents(renderer,rect.x+rect.w-24,rect.y+8)) return;
}
if(event.type==SDL_MOUSEBUTTONDOWN){
if(event.button.button==SDL_BUTTON_LEFT){
SDL_Rect mouse={event.button.x,event.button.y,0,0};
//Check if close it
if(!pointOnRect(mouse,rect)){
dismiss();
return;
}
//Check if item is clicked
if(highlightedObj!=NULL && highlightedBtn>0 && parent!=NULL){
//std::vector<Block*>& v=parent->levelObjects;
if(/*find(v.begin(),v.end(),highlightedObj)!=v.end()*/true/*???*/){
switch(highlightedBtn){
case 1:
{
std::vector<GameObject*>& v2=parent->selection;
std::vector<GameObject*>::iterator it=find(v2.begin(),v2.end(),highlightedObj);
if(it==v2.end()){
v2.push_back(highlightedObj);
}else{
v2.erase(it);
}
}
break;
case 2:
parent->commandManager->doCommand(new AddRemoveGameObjectCommand(parent, highlightedObj, false));
break;
case 3:
if(parent->actionsPopup)
delete parent->actionsPopup;
parent->actionsPopup=new LevelEditorActionsPopup(imageManager,renderer,parent,highlightedObj,mouse.x,mouse.y);
break;
}
}
}
}
}
else if(event.type == SDL_MOUSEWHEEL) {
//check mousewheel
if(event.wheel.y < 0){
startRow-=2;
updateScrollBar(imageManager,renderer);
return;
} else {
startRow+=2;
updateScrollBar(imageManager,renderer);
return;
}
}
}
};
/////////////////MovingPosition////////////////////////////
MovingPosition::MovingPosition(int x,int y,int time){
this->x=x;
this->y=y;
this->time=time;
}
MovingPosition::~MovingPosition(){}
void MovingPosition::updatePosition(int x,int y){
this->x=x;
this->y=y;
}
/////////////////LEVEL EDITOR//////////////////////////////
LevelEditor::LevelEditor(SDL_Renderer& renderer, ImageManager& imageManager):Game(renderer, imageManager){
//This will set some default settings.
reset();
//Create the GUI root.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Load the toolbar.
toolbar=imageManager.loadTexture(getDataPath()+"gfx/menu/toolbar.png",renderer);
toolbarRect={(SCREEN_WIDTH-460)/2,SCREEN_HEIGHT-50,460,50};
selectionPopup=NULL;
actionsPopup=NULL;
movingSpeedWidth=-1;
//Load the selectionMark.
selectionMark=imageManager.loadTexture(getDataPath()+"gfx/menu/selection.png",renderer);
//Load the movingMark.
movingMark=imageManager.loadTexture(getDataPath()+"gfx/menu/moving.png",renderer);
//Load the gui images.
bmGUI=imageManager.loadTexture(getDataPath()+"gfx/gui.png",renderer);
toolboxText=textureFromText(renderer,*fontText,_("Toolbox"),objThemes.getTextColor(true));
for(size_t i = 0;i < typeTextTextures.size();++i) {
typeTextTextures[i] =
textureFromText(renderer,
*fontText,
_(blockNames[i]),
objThemes.getTextColor(true));
}
for(size_t i = 0;i < tooltipTextures.size();++i) {
if (tooltipNames[i][0]) {
std::string s = _(tooltipNames[i]);
if (tooltipHotkey[i][0]) {
s += " (" + std::string(tooltipHotkey[i]) + ")";
} else if (tooltipHotkey2[i] >= 0) {
std::string s2 = InputManagerKeyCode::describeTwo(
inputMgr.getKeyCode((InputManagerKeys)tooltipHotkey2[i], false),
inputMgr.getKeyCode((InputManagerKeys)tooltipHotkey2[i], true));
if (!s2.empty()) s += " (" + s2 + ")";
}
tooltipTextures[i] = textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true));
}
}
//Count the level editing time.
statsMgr.startLevelEdit();
//Create the command manager with a maximum of 100 commands.
commandManager = new CommandManager(100);
}
LevelEditor::~LevelEditor(){
//Delete the command manager.
delete commandManager;
// NOTE: We don't need to delete levelObjects, etc. since they are deleted in Game::~Game().
// Clear selection
selection.clear();
//Delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Delete the popup
if(selectionPopup){
delete selectionPopup;
selectionPopup=NULL;
}
//Delete the popup
if(actionsPopup){
delete actionsPopup;
actionsPopup=NULL;
}
//Reset the camera.
camera.x=0;
camera.y=0;
//Count the level editing time.
statsMgr.endLevelEdit();
}
TexturePtr& LevelEditor::getCachedTextTexture(SDL_Renderer& renderer, const std::string& text) {
auto it = cachedTextTextures.find(text);
if (it != cachedTextTextures.end()) return it->second;
return (cachedTextTextures[text] = textureFromText(renderer,
*fontText,
text.c_str(),
objThemes.getTextColor(true)));
}
TexturePtr& LevelEditor::getSmallCachedTextTexture(SDL_Renderer& renderer, const std::string& text) {
auto it = cachedTextTextures.find("\x01small\x02" + text);
if (it != cachedTextTextures.end()) return it->second;
return (cachedTextTextures["\x01small\x02" + text] = textureFromText(renderer,
*fontMono,
text.c_str(),
objThemes.getTextColor(true)));
}
void LevelEditor::reset(){
//Set some default values.
playMode=false;
tool=ADD;
currentType=0;
toolboxVisible=false;
toolboxRect.x=-1;
toolboxRect.y=-1;
toolboxRect.w=0;
toolboxRect.h=0;
toolboxIndex=0;
pressedShift=false;
pressedLeftMouse=false;
dragging=false;
selectionDrag=-1;
dragCenter=NULL;
cameraXvel=0;
cameraYvel=0;
linking=false;
linkingTrigger=NULL;
currentId=0;
movingBlock=NULL;
moving=false;
movingSpeed=10;
pauseTime = 0;
tooltip=-1;
//Set the player and shadow to their starting position.
player.setLocation(player.fx,player.fy);
shadow.setLocation(shadow.fx,shadow.fy);
selection.clear();
clipboard.clear();
triggers.clear();
movingBlocks.clear();
//Delete any gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Clear the GUIWindow object map.
objectWindows.clear();
}
void LevelEditor::loadLevelFromNode(ImageManager& imageManager, SDL_Renderer& renderer,TreeStorageNode* obj, const std::string& fileName){
//call the method of base class.
Game::loadLevelFromNode(imageManager,renderer,obj,fileName);
//We swap the levelObjects and levelObjectsInitial again.
std::swap(levelObjects, levelObjectsInitial);
//now do our own stuff.
string s=editorData["time"];
if(s.empty() || !(s[0]>='0' && s[0]<='9')){
levelTime=-1;
}else{
levelTime=atoi(s.c_str());
}
s=editorData["recordings"];
if(s.empty() || !(s[0]>='0' && s[0]<='9')){
levelRecordings=-1;
}else{
levelRecordings=atoi(s.c_str());
}
levelTheme = editorData["theme"];
levelMusic = editorData["music"];
//NOTE: We set the camera here since we know the dimensions of the level.
if(levelRect.w<SCREEN_WIDTH)
camera.x=-(SCREEN_WIDTH-levelRect.w)/2;
else
camera.x=0;
if(levelRect.h<SCREEN_HEIGHT)
camera.y=-(SCREEN_HEIGHT-levelRect.h)/2;
else
camera.y=0;
//The level is loaded, so call postLoad.
postLoad();
}
void LevelEditor::saveLevel(string fileName){
//Create the output stream and check if it starts.
std::ofstream save(fileName.c_str());
if(!save) return;
//The current level.
LevelPack::Level *currentLevel = levels->getLevel(), *currentLevel2 = NULL;
//Check if the current level is individual level,
//in this case the level are both in "Levels" and "Custom Levels" level packs.
if (levels->type == COLLECTION) {
assert(levels->levelpackPath == CUSTOM_LEVELS_PATH);
if (auto levels2 = getLevelPackManager()->getLevelPack(LEVELS_PATH)) {
for (int i = 0, m = levels2->getLevelCount(); i < m; i++) {
if (levels2->getLevel(i)->file == currentLevel->file) {
currentLevel2 = levels2->getLevel(i);
break;
}
}
if (currentLevel2 == NULL) {
fprintf(stderr, "BUG: The custom level '%s' is not conatined in 'Levels' pack!\n", currentLevel->file.c_str());
}
}
}
//The storageNode to put the level data in before writing it away.
TreeStorageNode node;
char s[64];
//The name of the level.
if(!levelName.empty()){
node.attributes["name"].push_back(levelName);
//Update the level name in the levelpack.
currentLevel->name = levelName;
if (currentLevel2) currentLevel2->name = levelName;
}
//The level theme.
if(!levelTheme.empty())
node.attributes["theme"].push_back(levelTheme);
//The level music.
if (!levelMusic.empty())
node.attributes["music"].push_back(levelMusic);
//The arcade mode property.
node.attributes["arcade"].push_back(arcade ? "1" : "0");
currentLevel->arcade = arcade;
if (currentLevel2) currentLevel2->arcade = arcade;
//target time and recordings.
{
char c[32];
sprintf(c, "%d", std::max(levelTime, -1));
node.attributes["time"].push_back(c);
//Update the target time the levelpack.
currentLevel->targetTime = std::max(levelTime, -1);
if (currentLevel2) currentLevel2->targetTime = std::max(levelTime, -1);
sprintf(c, "%d", std::max(levelRecordings, -1));
node.attributes["recordings"].push_back(c);
//Update the target recordings the levelpack.
currentLevel->targetRecordings = std::max(levelRecordings, -1);
if (currentLevel2) currentLevel2->targetRecordings = std::max(levelRecordings, -1);
}
//The width of the level.
sprintf(s, "%d", levelRect.w);
node.attributes["size"].push_back(s);
//The height of the level.
sprintf(s, "%d", levelRect.h);
node.attributes["size"].push_back(s);
//Loop through the gameObjects and save them.
for(int o=0;o<(signed)levelObjects.size();o++){
int objectType=levelObjects[o]->type;
//Check if it's a legal gameObject type.
if(objectType>=0 && objectType<TYPE_MAX){
TreeStorageNode* obj1=new TreeStorageNode;
node.subNodes.push_back(obj1);
//It's a tile so name the node tile.
obj1->name="tile";
//Write away the type of the gameObject.
obj1->value.push_back(blockName[objectType]);
//Get the box for the location of the gameObject.
SDL_Rect box=levelObjects[o]->getBox(BoxType_Base);
//Put the location and size in the storageNode.
sprintf(s,"%d",box.x);
obj1->value.push_back(s);
sprintf(s,"%d",box.y);
obj1->value.push_back(s);
sprintf(s,"%d",box.w);
obj1->value.push_back(s);
sprintf(s,"%d",box.h);
obj1->value.push_back(s);
//Loop through the editor data and save it also.
vector<pair<string,string> > obj;
levelObjects[o]->getEditorData(obj);
for (const auto& o : obj) {
//Skip the data whose key or value is empty.
if (o.first.empty()) continue;
if (o.second.empty()) continue;
//Skip SOME data whose value is the default value. Currently only some boolean values are skipped.
//WARNING: When the default values are changed, these codes MUST be modified accordingly!!!
//NOTE: Currently we skip the "visible" property since it is used for every block and usually it's the default value.
if (o.first == "visible" && o.second == "1") continue;
#if 0
//NOTE: The following codes are more aggressive!!!
//if (o.first == "activated" && o.second == "1") continue; //moving blocks and conveyor belt // Don't use this because there was a "disabled" property
if (o.first == "loop" && o.second == "1") continue; //moving blocks and conveyor belt
if (o.first == "automatic" && o.second == "0") continue; //portal
#endif
//Save the data.
obj1->attributes[o.first].push_back(o.second);
}
//Loop through the scripts and add them to the storage node of the game object.
map<int,string>::iterator it;
Block* object=(dynamic_cast<Block*>(levelObjects[o]));
for(it=object->scripts.begin();it!=object->scripts.end();++it){
//Make sure the script isn't an empty string.
if(it->second.empty())
continue;
TreeStorageNode* script=new TreeStorageNode;
obj1->subNodes.push_back(script);
script->name="script";
script->value.push_back(gameObjectEventTypeMap[it->first]);
script->attributes["script"].push_back(it->second);
}
}
}
//Loop through the level scripts and save them.
for(auto it=scripts.begin();it!=scripts.end();++it){
//Make sure the script isn't an empty string.
if(it->second.empty())
continue;
TreeStorageNode* script=new TreeStorageNode;
node.subNodes.push_back(script);
script->name="script";
script->value.push_back(levelEventTypeMap[it->first]);
script->attributes["script"].push_back(it->second);
}
//Loop through the scenery layers and save them.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
TreeStorageNode* layer = new TreeStorageNode;
node.subNodes.push_back(layer);
layer->name = "scenerylayer";
layer->value.push_back(it->first);
it->second->saveToNode(layer);
}
//Create a POASerializer and write away the level node.
POASerializer objSerializer;
objSerializer.writeNode(&node,save,true,true);
//Invalidates the calculated MD5 for the level since the level is updated.
memset(currentLevel->md5Digest, 0, sizeof(currentLevel->md5Digest));
if (currentLevel2)
memset(currentLevel2->md5Digest, 0, sizeof(currentLevel2->md5Digest));
}
void LevelEditor::deselectAll() {
selection.clear();
dragCenter = NULL;
selectionDrag = -1;
selectionDirty();
}
///////////////EVENT///////////////////
void LevelEditor::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if we need to quit, if so we enter the exit state.
if(event.type==SDL_QUIT){
setNextState(STATE_EXIT);
}
//If playing/testing we should the game handle the events.
if(playMode){
Game::handleEvents(imageManager,renderer);
//Also check if we should exit the playMode.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
//Reset the game and disable playMode, disable script.
Game::reset(true, true);
//We swap the levelObjects and levelObjectsInitial again.
std::swap(levelObjects, levelObjectsInitial);
playMode=false;
GUIObjectRoot->visible=true;
camera.x=cameraSave.x;
camera.y=cameraSave.y;
//NOTE: To prevent the mouse to still "be pressed" we set it to false.
pressedLeftMouse=false;
}
}else{
//Also check if we should exit the editor.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
std::string s;
//Check if the file is changed
if (commandManager->isChanged()) {
s = _("The level has unsaved changes.");
s.push_back('\n');
}
//Before we quit ask a make sure question.
if(msgBox(imageManager,renderer,s+_("Are you sure you want to quit?"),MsgBoxYesNo,_("Quit prompt"))==MsgBoxYes){
//We exit the level editor.
setNextState(STATE_LEVEL_EDIT_SELECT);
//Play the menu music again.
getMusicManager()->playMusic("menu");
//No need for handling other events, so return.
return;
}
}
//Check if we should redirect the event to the actions popup
if(actionsPopup!=NULL){
actionsPopup->handleEvents(renderer);
return;
}
//Check if we should redirect the event to selection popup
if(selectionPopup!=NULL){
if(event.type==SDL_MOUSEBUTTONDOWN
|| event.type==SDL_MOUSEBUTTONUP
|| event.type==SDL_MOUSEMOTION)
{
selectionPopup->handleEvents(imageManager,renderer);
return;
}
}
//TODO: Don't handle any Events when GUIWindows process them.
{
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x,y,0,0};
for(unsigned int i=0;i<GUIObjectRoot->childControls.size();i++){
SDL_Rect box={0,0,0,0};
box.x=GUIObjectRoot->childControls[i]->left;
box.y=GUIObjectRoot->childControls[i]->top;
box.w=GUIObjectRoot->childControls[i]->width;
box.h=GUIObjectRoot->childControls[i]->height;
if(pointOnRect(mouse,box))
return;
}
}
//Check if toolbar is clicked.
if(event.type==SDL_MOUSEBUTTONDOWN && event.button.button==SDL_BUTTON_LEFT && tooltip>=0){
int t=tooltip;
if(t<NUMBER_TOOLS){
//Show/hide toolbox if the current mode is ADD and the user clicked ADD again.
if (tool == ADD && t == ADD) {
toolboxVisible = !toolboxVisible;
}
tool=(Tools)t;
//Stop linking or moving if the mode is not SELECT.
if (tool != SELECT) {
if (linking) {
linking = false;
linkingTrigger = NULL;
}
if (moving) {
moving = false;
movingBlock = NULL;
}
}
}else{
//The selected button isn't a tool.
//Now check which button it is.
if (t == (int)ToolTips::Play){
enterPlayMode(imageManager, renderer);
}
if (t == (int)ToolTips::LevelSettings){
//Open up level settings dialog
levelSettings(imageManager,renderer);
}
if (t == (int)ToolTips::BackToMenu){
//If the file is changed we show a confirmation dialog
if (commandManager->isChanged()) {
std::string s = _("The level has unsaved changes.");
s.push_back('\n');
if (msgBox(imageManager, renderer, s + _("Are you sure you want to quit?"), MsgBoxYesNo, _("Quit prompt")) != MsgBoxYes) return;
}
//Go back to the level selection screen of Level Editor
setNextState(STATE_LEVEL_EDIT_SELECT);
//Change the music back to menu music.
getMusicManager()->playMusic("menu");
}
if (t == (int)ToolTips::SaveLevel){
//Save current level
saveCurrentLevel(imageManager, renderer);
}
if (t == (int)ToolTips::UndoNoTooltip) {
commandManager->undo();
}
if (t == (int)ToolTips::RedoNoTooltip) {
commandManager->redo();
}
}
return;
}
//Check if tool box is clicked.
if(event.type==SDL_MOUSEBUTTONDOWN && event.button.button==SDL_BUTTON_LEFT && toolboxRect.w>0){
if(toolboxVisible){
if(event.button.y<64){
//Check if we need to hide it
if(event.button.x>=SCREEN_WIDTH-24 && event.button.x<SCREEN_WIDTH && event.button.y<20){
toolboxVisible=false;
return;
}
const int m = (SCREEN_WIDTH - 48) / 64;
//Check if a block is clicked.
if(event.button.x>=24 && event.button.x<SCREEN_WIDTH-24){
int i=(event.button.x-24)/64;
if(i<m && i+toolboxIndex<getEditorOrderMax()){
currentType=i+toolboxIndex;
}
}
//Check if move left button is clicked
if (event.button.x >= 0 && event.button.x < 24 && event.button.y >= 20 && event.button.y < 44) {
toolboxIndex -= m;
if (toolboxIndex < 0) toolboxIndex = 0;
}
//Check if move right button is clicked
if (event.button.x >= SCREEN_WIDTH - 24 && event.button.x < SCREEN_WIDTH && event.button.y >= 20 && event.button.y < 44) {
toolboxIndex += m;
if (toolboxIndex > getEditorOrderMax() - m) toolboxIndex = getEditorOrderMax() - m;
if (toolboxIndex < 0) toolboxIndex = 0;
}
return;
}
}else if(event.button.x>=toolboxRect.x && event.button.x<toolboxRect.x+toolboxRect.w
&& event.button.y>=toolboxRect.y && event.button.y<toolboxRect.y+toolboxRect.h)
{
toolboxVisible=true;
return;
}
}
//Check if shift is pressed.
pressedShift=inputMgr.isKeyDown(INPUTMGR_SHIFT);
//Check if delete is pressed.
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_DELETE){
if (!selection.empty()){
AddRemoveGameObjectCommand *command = new AddRemoveGameObjectCommand(this, selection, false);
//clear the selection vector first.
deselectAll();
//perform the actual deletion.
commandManager->doCommand(command);
}
}
//Check for copy (Ctrl+c) or cut (Ctrl+x).
if(event.type==SDL_KEYDOWN && (event.key.keysym.sym==SDLK_c || event.key.keysym.sym==SDLK_x) && (event.key.keysym.mod & KMOD_CTRL)){
//Check if the selection isn't empty.
if(!selection.empty()){
//Clear the current clipboard.
clipboard.clear();
//Loop through the selection to find the left-top block.
int x=selection[0]->getBox().x;
int y=selection[0]->getBox().y;
for(unsigned int o=1; o<selection.size(); o++){
if(selection[o]->getBox().x<x || selection[o]->getBox().y<y){
x=selection[o]->getBox().x;
y=selection[o]->getBox().y;
}
}
//Loop through the selection for the actual copying.
for(unsigned int o=0; o<selection.size(); o++){
//Get the editor data of the object.
vector<pair<string,string> > obj;
selection[o]->getEditorData(obj);
//Loop through the editor data and convert it.
map<string,string> objMap;
for(unsigned int i=0;i<obj.size();i++){
objMap[obj[i].first]=obj[i].second;
}
//Add some entries to the map.
char s[64];
SDL_Rect r = selection[o]->getBox();
sprintf(s, "%d", r.x - x);
objMap["x"]=s;
sprintf(s, "%d", r.y - y);
objMap["y"]=s;
sprintf(s, "%d", selection[o]->getBox().w);
objMap["w"] = s;
sprintf(s, "%d", selection[o]->getBox().h);
objMap["h"] = s;
if (Scenery *scenery = dynamic_cast<Scenery*>(selection[o])) {
objMap["sceneryName"] = scenery->sceneryName_;
objMap["customScenery"] = scenery->customScenery_;
} else {
sprintf(s, "%d", selection[o]->type);
objMap["type"] = s;
//Save scripts for block.
if (Block *block = dynamic_cast<Block*>(selection[o])) {
for (auto it = block->scripts.begin(); it != block->scripts.end(); ++it) {
sprintf(s, "_script.%d", it->first);
objMap[s] = it->second;
}
}
}
//Overwrite the id to prevent triggers, portals, buttons, movingblocks, etc. from malfunctioning.
//We give an empty string as id, which is invalid and thus suitable.
objMap["id"]="";
//Do the same for destination if the type is portal.
if(selection[o]->type==TYPE_PORTAL){
objMap["destination"]="";
}
//And add the map to the clipboard vector.
clipboard.push_back(objMap);
}
//Cutting means deleting the game object.
if (event.key.keysym.sym == SDLK_x && !selection.empty()){
AddRemoveGameObjectCommand *command = new AddRemoveGameObjectCommand(this, selection, false);
//clear the selection vector first.
deselectAll();
//perform the actual deletion.
commandManager->doCommand(command);
}
}
}
//Check for paste (Ctrl+v).
if(event.type==SDL_KEYDOWN && event.key.keysym.sym==SDLK_v && (event.key.keysym.mod & KMOD_CTRL)){
//First make sure that the clipboard isn't empty.
if(!clipboard.empty()){
//Clear the current selection.
deselectAll();
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
x+=camera.x;
y+=camera.y;
//Apply snap to grid.
if(!pressedShift){
snapToGrid(&x,&y);
}else{
x-=25;
y-=25;
}
std::vector<GameObject*> newObjects;
//Loop through the clipboard.
for(unsigned int o=0;o<clipboard.size();o++){
GameObject *obj = NULL;
if (clipboard[o].find("sceneryName") == clipboard[o].end()) {
// a normal block
if (!selectedLayer.empty()) continue;
Block *block = new Block(this, 0, 0, 50, 50, atoi(clipboard[o]["type"].c_str()));
obj = block;
// load the script for the block
for (auto it = clipboard[o].begin(); it != clipboard[o].end(); ++it) {
if (it->first.find("_script.") == 0) {
int eventType = atoi(it->first.c_str() + 8);
block->scripts[eventType] = it->second;
}
}
} else {
// a scenery block
if (selectedLayer.empty()) continue;
Scenery *scenery = new Scenery(this, 0, 0, 50, 50, clipboard[o]["sceneryName"]);
if (clipboard[o]["sceneryName"].empty()) {
scenery->customScenery_ = clipboard[o]["customScenery"];
scenery->updateCustomScenery(imageManager, renderer);
}
obj = scenery;
}
obj->setBaseLocation(atoi(clipboard[o]["x"].c_str())+x,atoi(clipboard[o]["y"].c_str())+y);
obj->setBaseSize(atoi(clipboard[o]["w"].c_str()), atoi(clipboard[o]["h"].c_str()));
obj->setEditorData(clipboard[o]);
//add the object.
newObjects.push_back(obj);
//Also add the block to the selection.
selection.push_back(obj);
}
// Do the actual object insertion
if (!newObjects.empty()) {
commandManager->doCommand(new AddRemoveGameObjectCommand(this, newObjects, true));
}
}
}
//Check for the arrow keys, used for moving the camera when playMode=false.
if (inputMgr.isKeyDown(INPUTMGR_RIGHT)) {
if (cameraXvel < 5) cameraXvel = 5;
} else if (inputMgr.isKeyDown(INPUTMGR_LEFT)) {
if (cameraXvel > -5) cameraXvel = -5;
} else {
cameraXvel = 0;
}
if(inputMgr.isKeyDown(INPUTMGR_DOWN)){
if (cameraYvel < 5) cameraYvel = 5;
} else if (inputMgr.isKeyDown(INPUTMGR_UP)){
if (cameraYvel > -5) cameraYvel = -5;
} else {
cameraYvel = 0;
}
//Check if the left mouse button is pressed/holded.
if(event.type==SDL_MOUSEBUTTONDOWN && event.button.button==SDL_BUTTON_LEFT){
pressedLeftMouse=true;
}
if(event.type==SDL_MOUSEBUTTONUP && event.button.button==SDL_BUTTON_LEFT){
pressedLeftMouse=false;
//We also need to check if dragging is true.
if(dragging){
//Set dragging false and call the onDrop event.
dragging=false;
int x,y;
SDL_GetMouseState(&x,&y);
//We call the drop event.
onDrop(x+camera.x,y+camera.y);
}
}
//Check if the mouse is dragging.
if(pressedLeftMouse && event.type==SDL_MOUSEMOTION) {
//Check if this is the start of the dragging.
if(!dragging){
//The mouse is moved enough so let's set dragging true.
dragging=true;
// NOTE: We start drag from previous mouse position to prevent resize area hit test bug
onDragStart(event.motion.x - event.motion.xrel + camera.x, event.motion.y - event.motion.yrel + camera.y);
onDrag(event.motion.xrel, event.motion.yrel);
} else {
//Dragging was already true meaning we call onDrag() instead of onDragStart().
onDrag(event.motion.xrel,event.motion.yrel);
}
}
//Update cursor.
if(dragging){
if (tool == REMOVE) {
currentCursor = CURSOR_REMOVE;
} else {
switch (selectionDrag) {
case 0: case 8:
currentCursor = CURSOR_SIZE_FDIAG;
break;
case 1: case 7:
currentCursor = CURSOR_SIZE_VER;
break;
case 2: case 6:
currentCursor = CURSOR_SIZE_BDIAG;
break;
case 3: case 5:
currentCursor = CURSOR_SIZE_HOR;
break;
case 4:
currentCursor = CURSOR_DRAG;
break;
default:
currentCursor = CURSOR_POINTER;
break;
}
}
}
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x,y,0,0};
//Check if we scroll up, meaning the currentType++;
if((event.type==SDL_MOUSEWHEEL && event.wheel.y > 0) || inputMgr.isKeyDownEvent(INPUTMGR_NEXT)){
switch(tool){
case ADD:
//Check if mouse is in tool box.
if(toolboxVisible && toolboxRect.w>0){
int x,y;
SDL_GetMouseState(&x,&y);
if(y<64){
const int m = getEditorOrderMax() - (SCREEN_WIDTH - 48) / 64;
toolboxIndex -= 2;
if (toolboxIndex>m) toolboxIndex = m;
if (toolboxIndex<0) toolboxIndex = 0;
break;
}
}
//Only change the current type when using the add tool.
currentType++;
if (currentType >= getEditorOrderMax()){
currentType=0;
}
break;
case SELECT:
//When configuring moving blocks.
if(moving){
if (pauseMode) {
//Here we have to use Ctrl because Shift means snap
pauseTime += (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
} else {
//Here we have to use Ctrl because Shift means snap
movingSpeed += (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
//The movingspeed is capped at 125 (10 block/s).
if (movingSpeed > 125){
movingSpeed = 125;
}
}
break;
}
//Fall through.
default:
//When in other mode, just scrolling the map
if (pressedShift) camera.x = clamp(camera.x - 200, -1000 - SCREEN_WIDTH, levelRect.w + 1000);
else camera.y = clamp(camera.y - 200, -1000 - SCREEN_HEIGHT, levelRect.h + 1000);
break;
}
}
//Check if we scroll down, meaning the currentType--;
if((event.type==SDL_MOUSEWHEEL && event.wheel.y < 0) || inputMgr.isKeyDownEvent(INPUTMGR_PREVIOUS)){
switch(tool){
case ADD:
//Check if mouse is in tool box.
if(toolboxVisible && toolboxRect.w>0){
int x,y;
SDL_GetMouseState(&x,&y);
if(y<64){
const int m = getEditorOrderMax() - (SCREEN_WIDTH - 48) / 64;
toolboxIndex+=2;
if(toolboxIndex>m) toolboxIndex=m;
if(toolboxIndex<0) toolboxIndex=0;
break;
}
}
//Only change the current type when using the add tool.
currentType--;
if(currentType<0){
currentType = getEditorOrderMax() - 1;
}
break;
case SELECT:
//When configuring moving blocks.
if(moving){
if (pauseMode) {
//Here we have to use Ctrl because Shift means snap
pauseTime -= (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
if (pauseTime < -1){
pauseTime = -1;
}
} else {
//Here we have to use Ctrl because Shift means snap
movingSpeed -= (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
if (movingSpeed <= 0){
movingSpeed = 1;
}
}
break;
}
//Fall through.
default:
//When in other mode, just scrolling the map
if (pressedShift) camera.x = clamp(camera.x + 200, -1000 - SCREEN_WIDTH, levelRect.w + 1000);
else camera.y = clamp(camera.y + 200, -1000 - SCREEN_HEIGHT, levelRect.h + 1000);
break;
}
}
if (event.type == SDL_KEYDOWN) {
bool unlink = false;
//Check if we should enter playMode.
if (event.key.keysym.sym == SDLK_F5){
enterPlayMode(imageManager, renderer);
}
//Check for tool shortcuts.
if (event.key.keysym.sym == SDLK_F2){
tool = SELECT;
}
if (event.key.keysym.sym == SDLK_F3){
//Show/hide toolbox if the current mode is ADD and the user clicked ADD again.
if (tool == ADD) {
toolboxVisible = !toolboxVisible;
}
tool = ADD;
unlink = true;
}
if (event.key.keysym.sym == SDLK_F4){
tool = REMOVE;
unlink = true;
}
//Stop linking or moving if the mode is not SELECT.
if (unlink) {
if (linking) {
linking = false;
linkingTrigger = NULL;
}
if (moving) {
moving = false;
movingBlock = NULL;
}
}
}
//Check for certain events.
//First make sure the mouse isn't above the toolbar.
if(!pointOnRect(mouse,toolbarRect) && !pointOnRect(mouse,toolboxRect)){
mouse.x+=camera.x;
mouse.y+=camera.y;
//Boolean if there's a click event fired.
bool clickEvent=false;
//Check if a mouse button is pressed.
if(event.type==SDL_MOUSEBUTTONDOWN){
//Right click in path or link mode means return to normal mode.
if (event.button.button == SDL_BUTTON_RIGHT && (linking || moving)) {
//Stop linking.
linking = false;
linkingTrigger = NULL;
//Stop moving.
moving = false;
movingBlock = NULL;
//Stop processing further.
return;
}
std::vector<GameObject*> clickObjects;
//Loop through the objects to check collision.
if (selectedLayer.empty()) {
if (layerVisibility[selectedLayer]) {
for (unsigned int o = 0; o<levelObjects.size(); o++){
if (pointOnRect(mouse, levelObjects[o]->getBox()) == true){
clickObjects.push_back(levelObjects[o]);
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
for (auto o : it->second->objects){
if (pointOnRect(mouse, o->getBox()) == true){
clickObjects.push_back(o);
}
}
}
}
//Check if there are multiple objects above eachother or just one.
if(clickObjects.size()==1){
//We have collision meaning that the mouse is above an object.
std::vector<GameObject*>::iterator it;
it=find(selection.begin(),selection.end(),clickObjects[0]);
//Set event true since there's a click event.
clickEvent=true;
//Check if the clicked object is in the selection or not.
bool isSelected=(it!=selection.end());
if(event.button.button==SDL_BUTTON_LEFT){
onClickObject(clickObjects[0],isSelected);
}else if(event.button.button==SDL_BUTTON_RIGHT){
onRightClickObject(imageManager,renderer,clickObjects[0],isSelected);
}
}else if(clickObjects.size()>=1){
//There are more than one object under the mouse
//SDL2 port (never managed to trigger this without changing the parameters.
std::vector<GameObject*>::iterator it;
it=find(selection.begin(),selection.end(),clickObjects[0]);
//Set event true since there's a click event.
clickEvent=true;
//Check if the clicked object is in the selection or not.
bool isSelected=(it!=selection.end());
//Only show the selection popup when right clicking.
if(event.button.button==SDL_BUTTON_LEFT){
onClickObject(clickObjects[0],isSelected);
}else if(event.button.button==SDL_BUTTON_RIGHT){
//Remove the selection popup if there's one.
if(selectionPopup!=NULL)
delete selectionPopup;
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
selectionPopup=new LevelEditorSelectionPopup(this,imageManager,renderer,clickObjects,x,y);
}
}
}
//If event is false then we clicked on void.
if(!clickEvent){
if(event.type==SDL_MOUSEBUTTONDOWN){
if(event.button.button==SDL_BUTTON_LEFT){
//Left mouse button on void.
onClickVoid(mouse.x,mouse.y);
}else if(event.button.button==SDL_BUTTON_RIGHT){
onRightClickVoid(imageManager,renderer,mouse.x,mouse.y);
}
}
}
}
//Check for backspace when moving to remove a movingposition.
if(moving && event.type==SDL_KEYDOWN && event.key.keysym.sym==SDLK_BACKSPACE){
if(movingBlocks[movingBlock].size()>0){
commandManager->doCommand(new AddRemovePathCommand(this, movingBlock, MovingPosition(0, 0, 0), false));
}
}
//Check for the tab key, level settings.
if(inputMgr.isKeyDownEvent(INPUTMGR_TAB)){
//Show the levelSettings.
levelSettings(imageManager,renderer);
}
//Check if we should save the level (Ctrl+s).
if(event.type==SDL_KEYDOWN && event.key.keysym.sym==SDLK_s && (event.key.keysym.mod & KMOD_CTRL)){
saveCurrentLevel(imageManager, renderer);
}
//Undo ctrl+z
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_z && (event.key.keysym.mod & KMOD_CTRL)){
undo();
}
//Redo ctrl+y
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_y && (event.key.keysym.mod & KMOD_CTRL)){
redo();
}
}
}
void LevelEditor::saveCurrentLevel(ImageManager& imageManager, SDL_Renderer& renderer) {
saveLevel(levelFile);
//Clear the dirty flag
commandManager->resetChange();
//And give feedback to the user.
if (levelName.empty())
msgBox(imageManager, renderer, tfm::format(_("Level \"%s\" saved"), fileNameFromPath(levelFile)), MsgBoxOKOnly, _("Saved"));
else
msgBox(imageManager, renderer, tfm::format(_("Level \"%s\" saved"), levelName), MsgBoxOKOnly, _("Saved"));
}
void LevelEditor::updateRecordInPlayMode(ImageManager& imageManager, SDL_Renderer& renderer) {
bool update = false;
if (currentTime < 0 || currentRecordings < 0) {
currentTime = time;
currentRecordings = arcade ? currentCollectables : recordings;
update = true;
}
int newTime, newRecordings;
if (time < 0) newTime = bestTime;
else if (bestTime < 0) newTime = time;
else if (arcade) newTime = std::max(time, bestTime);
else newTime = std::min(time, bestTime);
if (arcade) {
if (currentCollectables < 0) newRecordings = bestRecordings;
else if (bestRecordings < 0) newRecordings = currentCollectables;
else newRecordings = std::max(currentCollectables, bestRecordings);
} else {
if (recordings < 0) newRecordings = bestRecordings;
else if (bestRecordings < 0) newRecordings = recordings;
else newRecordings = std::min(recordings, bestRecordings);
}
if (update || newTime != bestTime || newRecordings != bestRecordings) {
bestTime = newTime;
bestRecordings = newRecordings;
updateAdditionalTexture(imageManager, renderer);
}
}
void LevelEditor::updateAdditionalTexture(ImageManager& imageManager, SDL_Renderer& renderer) {
SDL_Color color = objThemes.getTextColor(true);
std::vector<SurfacePtr> v[3];
if (currentTime >= 0 && currentRecordings >= 0) {
v[0].emplace_back(TTF_RenderUTF8_Blended(fontMono,
tfm::format(_("Time: %-.2fs"), currentTime / 40.0).c_str(),
color));
v[1].emplace_back(TTF_RenderUTF8_Blended(fontMono,
tfm::format(arcade ? _("Collectibles: %d") : _("Recordings: %d"), currentRecordings).c_str(),
color));
}
if (bestTime >= 0 && bestRecordings >= 0) {
v[0].emplace_back(TTF_RenderUTF8_Blended(fontMono,
tfm::format(_("Best time: %-.2fs"), bestTime / 40.0).c_str(),
color));
v[1].emplace_back(TTF_RenderUTF8_Blended(fontMono,
tfm::format(arcade ? _("Best collectibles: %d") : _("Best recordings: %d"), bestRecordings).c_str(),
color));
int medal = LevelPack::Level::getMedal(arcade, bestTime, levelTime, bestRecordings, levelRecordings);
v[2].emplace_back(TTF_RenderUTF8_Blended(fontMono,
tfm::format(_("You earned the %s medal"), (medal > 1) ? (medal == 3) ? _("GOLD") : _("SILVER") : _("BRONZE")).c_str(),
color));
}
if (levelTime >= 0) {
v[0].emplace_back(TTF_RenderUTF8_Blended(fontMono,
tfm::format(_("Target time: %-.2fs"), levelTime / 40.0).c_str(),
color));
}
if (levelRecordings >= 0) {
v[1].emplace_back(TTF_RenderUTF8_Blended(fontMono,
tfm::format(arcade ? _("Target collectibles: %d") : _("Target recordings: %d"), levelRecordings).c_str(),
color));
}
if (v[0].empty() && v[1].empty()) {
additionalTexture = NULL;
return;
}
//calculate size
int ww[2] = { 0, 0 }, w = 0, h = 0;
for (int i = 0; i < 2; i++) {
int h0 = 0;
for (auto &s : v[i]) {
if (ww[i] < s->w) ww[i] = s->w;
h0 += s->h;
}
if (h < h0) h = h0;
}
w = ww[0] + ww[1] + 24;
int h2 = 0;
for (auto &s : v[2]) {
if (w < s->w + 8) w = s->w + 8;
h2 += s->h;
}
//create surface
SurfacePtr surf(createSurface(w, h + h2));
int y = 0;
for (auto &s : v[0]) {
SDL_SetSurfaceBlendMode(s.get(), SDL_BLENDMODE_NONE);
applySurface(0, y, s.get(), surf.get(), NULL);
y += s->h;
}
y = 0;
for (auto &s : v[1]) {
SDL_SetSurfaceBlendMode(s.get(), SDL_BLENDMODE_NONE);
applySurface(ww[0] + 16, y, s.get(), surf.get(), NULL);
y += s->h;
}
y = h;
for (auto &s : v[2]) {
SDL_SetSurfaceBlendMode(s.get(), SDL_BLENDMODE_NONE);
applySurface((w - s->w) / 2, y, s.get(), surf.get(), NULL);
y += s->h;
}
//over
additionalTexture = textureFromSurface(renderer, std::move(surf));
}
void LevelEditor::enterPlayMode(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if we are already in play mode.
if(playMode) return;
//Reset statistics.
currentTime = currentRecordings = bestTime = bestRecordings = -1;
//Stop linking or moving.
if(linking){
linking=false;
linkingTrigger=NULL;
}
if(moving){
moving=false;
movingBlock=NULL;
}
//Recalculate the number of collectibles.
totalCollectables = totalCollectablesSaved = totalCollectablesInitial = 0;
//We need to reposition player and shadow here, since the related code is removed from object placement.
for (auto obj : levelObjects) {
//If the object is a player or shadow start then change the start position of the player or shadow.
if (obj->type == TYPE_START_PLAYER){
//Center the player horizontally.
player.fx = obj->getBox().x + (obj->getBox().w - player.getBox().w) / 2;
player.fy = obj->getBox().y;
}
if (obj->type == TYPE_START_SHADOW){
//Center the shadow horizontally.
shadow.fx = obj->getBox().x + (obj->getBox().w - shadow.getBox().w) / 2;
shadow.fy = obj->getBox().y;
}
if (obj->type == TYPE_COLLECTABLE) {
totalCollectablesSaved = totalCollectablesInitial = ++totalCollectables;
}
}
//Change mode.
playMode=true;
GUIObjectRoot->visible=false;
cameraSave.x=camera.x;
cameraSave.y=camera.y;
//We swap the levelObjects and levelObjectsInitial again.
std::swap(levelObjects, levelObjectsInitial);
//Restart the game with scripting enabled.
Game::reset(true, false);
}
void LevelEditor::undo(){
commandManager->undo();
}
void LevelEditor::redo(){
commandManager->redo();
}
void LevelEditor::levelSettings(ImageManager& imageManager,SDL_Renderer& renderer){
//It isn't so open a popup asking for a name.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-500)/2,600,500,true,true,_("Level settings"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="lvlSettingsWindow";
root->eventCallback=this;
GUIObject* obj;
//Create the two textboxes with a label.
obj=new GUILabel(imageManager,renderer,40,60,240,36,_("Name:"));
root->addChild(obj);
obj=new GUITextBox(imageManager,renderer,140,60,410,36,levelName.c_str());
obj->gravityRight = GUIGravityRight;
obj->name="name";
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 110, 240, 36, (std::string("* ") + _("Theme:")).c_str());
root->addChild(obj);
obj=new GUITextBox(imageManager,renderer,140,110,410,36,levelTheme.c_str());
obj->gravityRight = GUIGravityRight;
obj->name="theme";
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 150, 510, 36, _("Examples: %DATA%/themes/classic"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 174, 510, 36, _("or %USER%/themes/Orange"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 210, 240, 36, _("Music:"));
root->addChild(obj);
obj = new GUITextBox(imageManager, renderer, 140, 210, 410, 36, levelMusic.c_str());
obj->gravityRight = GUIGravityRight;
obj->name = "music";
root->addChild(obj);
//arcade mode check box.
obj = new GUICheckBox(imageManager, renderer, 40, 260, 240, 36, _("Arcade mode"));
obj->name = "chkArcadeMode";
obj->value = arcade ? 1 : 0;
obj->eventCallback = root;
root->addChild(obj);
//target time and recordings.
{
obj=new GUICheckBox(imageManager,renderer,40,310,240,36,_("Target time (s):"));
obj->name = "chkTime";
obj->value = levelTime >= 0 ? 1 : 0;
obj->eventCallback = root;
root->addChild(obj);
GUISpinBox* obj2=new GUISpinBox(imageManager,renderer,290,310,260,36);
obj2->gravityRight = GUIGravityRight;
obj2->name="time";
char ss[128];
sprintf(ss, "%0.2f", double(levelTime >= 0 ? levelTime : ~levelTime) / 40.0);
obj2->caption=ss;
obj2->visible = levelTime >= 0;
obj2->limitMin=0.0f;
obj2->limitMax = 1E+6f;
obj2->format = "%0.2f";
obj2->change=0.1f;
obj2->update();
root->addChild(obj2);
obj=new GUICheckBox(imageManager,renderer,40,360,240,36,arcade?_("Target collectibles:"):_("Target recordings:"));
obj->name = "chkRecordings";
obj->value = levelRecordings >= 0 ? 1 : 0;
obj->eventCallback = root;
root->addChild(obj);
obj2=new GUISpinBox(imageManager,renderer,290,360,260,36);
obj2->gravityRight = GUIGravityRight;
sprintf(ss, "%d", levelRecordings >= 0 ? levelRecordings : ~levelRecordings);
obj2->caption=ss;
obj2->visible = levelRecordings >= 0;
obj2->limitMin=0.0f;
obj2->limitMax = 1E+6f;
obj2->format="%1.0f";
obj2->name="recordings";
obj2->update();
root->addChild(obj2);
}
obj = new GUILabel(imageManager, renderer, 40, 400, 510, 36, (std::string("* ") + _("Restart level editor is required")).c_str());
root->addChild(obj);
//Ok and cancel buttons.
obj=new GUIButton(imageManager,renderer,root->width*0.3,500-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="lvlSettingsOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,500-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="lvlSettingsCancel";
obj->eventCallback=root;
root->addChild(obj);
GUIObjectRoot->addChild(root);
}
void LevelEditor::postLoad(){
//We need to find the triggers.
for(unsigned int o=0;o<levelObjects.size();o++){
//Check for the highest id.
unsigned int id=atoi(levelObjects[o]->getEditorProperty("id").c_str());
if(id>=currentId)
currentId=id+1;
switch(levelObjects[o]->type){
case TYPE_BUTTON:
case TYPE_SWITCH:
{
//Add the object to the triggers vector.
vector<GameObject*> linked;
triggers[levelObjects[o]]=linked;
//Now loop through the levelObjects in search for objects with the same id.
for(unsigned int oo=0;oo<levelObjects.size();oo++){
//Check if it isn't the same object but has the same id.
if(o!=oo && (dynamic_cast<Block*>(levelObjects[o]))->id==(dynamic_cast<Block*>(levelObjects[oo]))->id){
//Add the object to the link vector of the trigger.
triggers[levelObjects[o]].push_back(levelObjects[oo]);
}
}
break;
}
case TYPE_PORTAL:
{
//Add the object to the triggers vector.
vector<GameObject*> linked;
triggers[levelObjects[o]]=linked;
//If the destination is empty we return.
if((dynamic_cast<Block*>(levelObjects[o]))->destination.empty()){
break;
}
//Now loop through the levelObjects in search for objects with the same id as destination.
for(unsigned int oo=0;oo<levelObjects.size();oo++){
//Check if it isn't the same object but has the same id.
if(o!=oo && (dynamic_cast<Block*>(levelObjects[o]))->destination==(dynamic_cast<Block*>(levelObjects[oo]))->id){
//Add the object to the link vector of the trigger.
triggers[levelObjects[o]].push_back(levelObjects[oo]);
}
}
break;
}
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
//Get the moving position.
const vector<SDL_Rect> &movingPos = levelObjects[o]->movingPos;
//Add the object to the movingBlocks vector.
movingBlocks[levelObjects[o]].clear();
for (int i = 0, m = movingPos.size(); i < m; i++) {
MovingPosition position(movingPos[i].x, movingPos[i].y, movingPos[i].w);
movingBlocks[levelObjects[o]].push_back(position);
}
break;
}
default:
break;
}
}
// Set the visibility of all layers to true
layerVisibility.clear();
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
layerVisibility[it->first] = true;
}
layerVisibility[std::string()] = true;
// Also set the current layer to the Block layer
selectedLayer.clear();
// Get all available scenery blocks
std::set<std::string> tmpset;
objThemes.getSceneryBlockNames(tmpset);
sceneryBlockNames.clear();
sceneryBlockNames.insert(sceneryBlockNames.end(), tmpset.begin(), tmpset.end());
}
void LevelEditor::snapToGrid(int* x,int* y){
//Check if the x location is negative.
if(*x<0){
*x=-((abs(*x-50)/50)*50);
}else{
*x=(*x/50)*50;
}
//Now the y location.
if(*y<0){
*y=-((abs(*y-50)/50)*50);
}else{
*y=(*y/50)*50;
}
}
void LevelEditor::setCamera(const SDL_Rect* r,int count){
//SetCamera only works in the Level editor and when mouse is inside window.
if(stateID==STATE_LEVEL_EDITOR&&(SDL_GetMouseFocus() == sdlWindow)){
//Get the mouse coordinates.
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
//Don't continue here if mouse is inside one of the boxes given as parameter.
for(int i=0;i<count;i++){
if(pointOnRect(mouse,r[i]))
return;
}
//FIXME: two ad-hoc camera speed variables similar to cameraXvel and cameraYvel
static int cameraXvelB = 0, cameraYvelB = 0;
//Check if the mouse is near the left edge of the screen.
//Else check if the mouse is near the right edge.
if (x < 50) {
//We're near the left edge so move the camera.
if (cameraXvelB > -5) cameraXvelB = -5;
if (pressedShift) cameraXvelB--;
} else if (x > SCREEN_WIDTH - 50) {
//We're near the right edge so move the camera.
if (cameraXvelB < 5) cameraXvelB = 5;
if (pressedShift) cameraXvelB++;
} else {
cameraXvelB = 0;
}
//Check if the tool box is visible and we need to calc screen size correctly.
int y0=50;
if (toolboxVisible && toolboxRect.w > 0) y0 += toolbarRect.h;
//Check if the mouse is near the top edge of the screen.
//Else check if the mouse is near the bottom edge.
if (y < y0) {
//We're near the top edge so move the camera.
if (cameraYvelB > -5) cameraYvelB = -5;
if (pressedShift) cameraYvelB--;
} else if (y > SCREEN_HEIGHT - 50) {
//We're near the bottom edge so move the camera.
if (cameraYvelB < 5) cameraYvelB = 5;
if (pressedShift) cameraYvelB++;
} else {
cameraYvelB = 0;
}
camera.x = clamp(camera.x + cameraXvelB, -1000 - SCREEN_WIDTH, levelRect.w + 1000);
camera.y = clamp(camera.y + cameraYvelB, -1000 - SCREEN_HEIGHT, levelRect.h + 1000);
}
}
void LevelEditor::onClickObject(GameObject* obj,bool selected){
switch(tool){
case SELECT:
{
//Check if we are linking.
if(linking){
//Check if the obj is valid to link to.
switch(obj->type){
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
//It's only valid when not linking a portal.
if(linkingTrigger->type==TYPE_PORTAL){
//You can't link a portal to moving blocks, etc.
//Stop linking and return.
linkingTrigger=NULL;
linking=false;
return;
}
break;
}
case TYPE_PORTAL:
{
//Make sure that the linkingTrigger is also a portal.
if(linkingTrigger->type!=TYPE_PORTAL){
//The linkingTrigger isn't a portal so stop linking and return.
linkingTrigger=NULL;
linking=false;
return;
}
break;
}
default:
//It isn't valid so stop linking and return.
linkingTrigger=NULL;
linking=false;
return;
break;
}
AddLinkCommand* pCommand = new AddLinkCommand(this, linkingTrigger, obj);
commandManager->doCommand(pCommand);
//We return to prevent configuring stuff like conveyor belts, etc...
linking=false;
linkingTrigger=NULL;
return;
}
//If we're moving add a movingposition.
if(moving){
//Get the current mouse location.
int x, y;
SDL_GetMouseState(&x, &y);
x += camera.x;
y += camera.y;
addMovingPosition(x, y);
return;
}
}
case ADD:
{
//Check if object is already selected.
if(!selected){
//First check if shift is pressed or not.
if(!pressedShift){
//Clear the selection.
deselectAll();
}
//Add the object to the selection.
selection.push_back(obj);
}
break;
}
case REMOVE:
{
//Remove the object.
commandManager->doCommand(new AddRemoveGameObjectCommand(this, obj, false));
break;
}
default:
break;
}
}
void LevelEditor::addMovingPosition(int x,int y) {
//Apply snap to grid.
if (!pressedShift){
snapToGrid(&x, &y);
} else{
x -= 25;
y -= 25;
}
x -= movingBlock->getBox().x;
y -= movingBlock->getBox().y;
//Calculate the length.
//First get the delta x and y.
int dx, dy;
if (movingBlocks[movingBlock].empty()){
dx = x;
dy = y;
} else{
dx = x - movingBlocks[movingBlock].back().x;
dy = y - movingBlocks[movingBlock].back().y;
}
AddRemovePathCommand* pCommand = NULL;
if (dx == 0 && dy == 0) {
// pause mode
if (pauseTime != 0) pCommand = new AddRemovePathCommand(this, movingBlock, MovingPosition(x, y, std::max(pauseTime, 0)), true);
pauseTime = 0;
} else {
// add new point mode
const double length = sqrt(double(dx*dx + dy*dy));
pCommand = new AddRemovePathCommand(this, movingBlock, MovingPosition(x, y, (int)(length*(10 / (double)movingSpeed))), true);
}
if (pCommand) commandManager->doCommand(pCommand);
}
void LevelEditor::onRightClickObject(ImageManager& imageManager,SDL_Renderer& renderer,GameObject* obj,bool){
//Create an actions popup for the game object.
if(actionsPopup==NULL){
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
actionsPopup=new LevelEditorActionsPopup(imageManager,renderer,this,obj,x,y);
return;
}
}
void LevelEditor::onClickVoid(int x,int y){
switch(tool){
case ADD:
{
//We need to clear the selection.
deselectAll();
//Now place an object.
//Apply snap to grid.
if(!pressedShift){
snapToGrid(&x,&y);
}else{
x-=25;
y-=25;
}
if (currentType >= 0 && currentType < getEditorOrderMax()) {
GameObject *obj;
if (selectedLayer.empty()) {
obj = new Block(this, x, y, 50, 50, editorTileOrder[currentType]);
} else {
obj = new Scenery(this, x, y, 50, 50,
currentType < (int)sceneryBlockNames.size() ? sceneryBlockNames[currentType] : std::string());
}
commandManager->doCommand(new AddRemoveGameObjectCommand(this, obj, true));
}
break;
}
case SELECT:
{
//We need to clear the selection.
deselectAll();
//If we're linking we should stop, user abort.
if(linking){
linking=false;
linkingTrigger=NULL;
//And return.
return;
}
//If we're moving we should add a point.
if(moving){
addMovingPosition(x, y);
//And return.
return;
}
break;
}
default:
break;
}
}
void LevelEditor::onRightClickVoid(ImageManager& imageManager,SDL_Renderer& renderer,int,int){
//Create an actions popup for the game object.
if(actionsPopup==NULL){
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
actionsPopup=new LevelEditorActionsPopup(imageManager,renderer,this,NULL,x,y);
return;
}
}
void LevelEditor::onDragStart(int x,int y){
switch(tool){
case SELECT:
case ADD:
{
//We can drag the selection so check if the selection isn't empty.
if(!selection.empty()){
//The selection isn't empty so search the dragCenter.
//Create a mouse rectangle.
SDL_Rect mouse={x,y,0,0};
// record the drag start position
dragSrartPosition.x = x;
dragSrartPosition.y = y;
//Loop through the objects to check collision.
for(unsigned int o=0; o<selection.size(); o++){
SDL_Rect r = selection[o]->getBox();
if(pointOnRect(mouse, r)){
//We have collision so set the dragCenter.
dragCenter=selection[o];
// determine which part is dragged
selectionDrag = 4;
int midx = r.x + r.w / 2 - 2;
int midy = r.y + r.h / 2 - 2;
if (mouse.x >= r.x && mouse.x < r.x + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
selectionDrag = 0;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
selectionDrag = 3;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
selectionDrag = 6;
}
} else if (mouse.x >= midx && mouse.x < midx + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
selectionDrag = 1;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
selectionDrag = 7;
}
} else if (mouse.x >= r.x + r.w - 5 && mouse.x < r.x + r.w) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
selectionDrag = 2;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
selectionDrag = 5;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
selectionDrag = 8;
}
}
break;
}
}
}
break;
}
default:
break;
}
}
void LevelEditor::onDrag(int dx,int dy){
switch(tool){
case REMOVE:
{
//No matter what we delete the item the mouse is above.
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x+camera.x,y+camera.y,0,0};
currentCursor=CURSOR_REMOVE;
std::vector<GameObject*> objects;
//Loop through the objects to check collision.
if (selectedLayer.empty()) {
if (layerVisibility[selectedLayer]) {
for (unsigned int o = 0; o<levelObjects.size(); o++){
if (pointOnRect(mouse, levelObjects[o]->getBox()) == true){
objects.push_back(levelObjects[o]);
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
for (auto o : it->second->objects){
if (pointOnRect(mouse, o->getBox()) == true){
objects.push_back(o);
}
}
}
}
// Do the actual object deletion.
if (!objects.empty()) {
commandManager->doCommand(new AddRemoveGameObjectCommand(this, objects, false));
}
break;
}
default:
break;
}
}
void LevelEditor::onDrop(int x,int y){
switch(tool){
case SELECT:
case ADD:
{
//Check if the drag center isn't null.
if(dragCenter==NULL)
return;
//The location of the dragCenter.
SDL_Rect r=dragCenter->getBox();
if (selectionDrag == 4) { // dragging
//Apply snap to grid.
determineNewPosition(x, y);
commandManager->doCommand(new MoveGameObjectCommand(this, selection, x - r.x, y - r.y));
} else if (selectionDrag >= 0) { // resizing
determineNewSize(x, y, r);
commandManager->doCommand(new MoveGameObjectCommand(this, dragCenter, r.x, r.y, r.w, r.h));
}
//Make sure the dragCenter is null and set selectionDrag false.
dragCenter=NULL;
selectionDrag=-1;
break;
}
default:
break;
}
}
void LevelEditor::onCameraMove(int dx,int dy){
switch(tool){
case REMOVE:
{
//Only delete when the left mouse button is pressed.
if(pressedLeftMouse){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x+camera.x,y+camera.y,0,0};
std::vector<GameObject*> objects;
//Loop through the objects to check collision.
if (selectedLayer.empty()) {
if (layerVisibility[selectedLayer]) {
for (unsigned int o = 0; o<levelObjects.size(); o++){
if (pointOnRect(mouse, levelObjects[o]->getBox()) == true){
objects.push_back(levelObjects[o]);
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
for (auto o : it->second->objects){
if (pointOnRect(mouse, o->getBox()) == true){
objects.push_back(o);
}
}
}
}
// Do the actual object deletion.
if (!objects.empty()) {
commandManager->doCommand(new AddRemoveGameObjectCommand(this, objects, false));
}
}
break;
}
default:
break;
}
}
void LevelEditor::selectionDirty() {
if (selectionPopup != NULL) selectionPopup->dirty = true;
}
void LevelEditor::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Check if one of the windows is closed.
if (eventType == GUIEventClick && name.size() >= 6 && name.substr(name.size() - 6) == "Window") {
destroyWindow(obj);
return;
}
//Resize code for each GUIWindow.
if (name.size() >= 6 && name.substr(name.size() - 6) == "Window") {
//Currently we don't need to process custom resize code since they are already processed in GUIWindow::resize().
return;
}
//Check for GUI events.
//Notification block configure events.
if(name=="cfgNotificationBlockOK"){
//Get the configuredObject.
if (GameObject* configuredObject = objectWindows[obj]) {
//Get the message textbox from the GUIWindow.
if (auto subwidget = obj->getChild("message")) {
if (auto message = dynamic_cast<GUITextArea*>(subwidget)) {
+ assert(configuredObject->type == TYPE_NOTIFICATION_BLOCK);
//Set the message of the notification block.
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
- configuredObject, "message", message->getString(), _("Message")));
+ configuredObject, "message", escapeNewline(message->getString()), _("Message")));
} else {
+ assert(configuredObject->type != TYPE_NOTIFICATION_BLOCK);
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
configuredObject, "message", subwidget->caption, _("Message")));
}
}
}
}
//Conveyor belt block configure events.
else if(name=="cfgConveyorBlockOK"){
//Get the configuredObject.
GameObject* configuredObject=objectWindows[obj];
if(configuredObject){
//Get the speed textbox from the GUIWindow.
GUISpinBox* speed=(GUISpinBox*)obj->getChild("speed");
if(speed){
//Set the speed of the conveyor belt.
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
configuredObject, "speed10", speed->caption, _("Speed")));
}
}
}
else if (name == "chkArcadeMode") {
obj->getChild("chkRecordings")->caption = obj->getChild("chkArcadeMode")->value ? _("Target collectibles:") : _("Target recordings:");
return;
}
else if (name == "chkTime") {
obj->getChild("time")->visible = obj->getChild("chkTime")->value ? 1 : 0;
return;
}
else if (name == "chkRecordings") {
obj->getChild("recordings")->visible = obj->getChild("chkRecordings")->value ? 1 : 0;
return;
}
//LevelSetting events.
else if(name=="lvlSettingsOK"){
SetLevelPropertyCommand::LevelProperty prop;
prop.levelTime = -1;
prop.levelRecordings = -1;
GUIObject* object=obj->getChild("name");
if(object)
prop.levelName=object->caption;
object=obj->getChild("theme");
if(object)
prop.levelTheme=object->caption;
object = obj->getChild("music");
if (object)
prop.levelMusic = object->caption;
//arcade mode.
object = obj->getChild("chkArcadeMode");
if (object)
prop.arcade = object->value;
//target time and recordings.
object = obj->getChild("chkTime");
GUISpinBox* object2 = dynamic_cast<GUISpinBox*>(obj->getChild("time"));
assert(object && object2);
double number = std::max(atof(object2->caption.c_str()), 0.0);
prop.levelTime = int(floor(number*40.0 + 0.5));
if (object->value == 0) prop.levelTime = ~prop.levelTime;
object = obj->getChild("chkRecordings");
object2 = dynamic_cast<GUISpinBox*>(obj->getChild("recordings"));
assert(object && object2);
prop.levelRecordings = std::max(atoi(object2->caption.c_str()), 0);
if (object->value == 0) prop.levelRecordings = ~prop.levelRecordings;
// Perform the level setting modification
commandManager->doCommand(new SetLevelPropertyCommand(this, prop));
}
//Level scripting window events.
else if(name=="cfgLevelScriptingEventType"){
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=(GUISingleLineListBox*)obj->getChild("cfgLevelScriptingEventType");
if(list){
//Loop through the scripts.
for(unsigned int i=0;i<list->item.size();i++){
GUIObject* script=obj->getChild(list->item[i].first);
if(script){
script->visible=(script->name==list->item[list->value].first);
script->enabled=(script->name==list->item[list->value].first);
}
}
}
return;
}
else if(name=="cfgLevelScriptingOK"){
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=(GUISingleLineListBox*)obj->getChild("cfgLevelScriptingEventType");
if(list){
std::map<int, std::string> newScript;
//Loop through the scripts.
for(unsigned int i=0;i<list->item.size();i++){
//Get the GUITextArea.
GUITextArea* script=dynamic_cast<GUITextArea*>(obj->getChild(list->item[i].first));
if(script){
//Set the script for the target block.
string str=script->getString();
if(!str.empty())
newScript[levelEventNameMap[script->name]]=str;
}
}
// Check achievement
if (!newScript.empty()) {
statsMgr.newAchievement("helloworld");
}
// Do the actual changes
commandManager->doCommand(new SetScriptCommand(this, NULL, newScript));
}
}
//Scripting window events.
else if (name == "cfgScriptingEventType"){
//TODO: Save any unsaved scripts? (Or keep track of all scripts and save upon cfgScriptingOK?)
//Get the configuredObject.
Block* configuredObject=dynamic_cast<Block*>(objectWindows[obj]);
if(configuredObject){
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=dynamic_cast<GUISingleLineListBox*>(obj->getChild("cfgScriptingEventType"));
if(list){
//Loop through the scripts.
for(unsigned int i=0;i<list->item.size();i++){
GUIObject* script=obj->getChild(list->item[i].first);
if(script){
script->visible=script->enabled=(script->name==list->item[list->value].first);
}
}
}
}
return;
}
else if(name=="cfgScriptingOK"){
//Get the configuredObject.
Block* block = dynamic_cast<Block*>(objectWindows[obj]);
if (block){
std::map<int, std::string> newScript;
std::string newId;
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=(GUISingleLineListBox*)obj->getChild("cfgScriptingEventType");
GUIObject* id=obj->getChild("id");
if (list){
//Loop through the scripts.
for (unsigned int i = 0; i < list->item.size(); i++){
//Get the GUITextArea.
GUITextArea* script = dynamic_cast<GUITextArea*>(obj->getChild(list->item[i].first));
if (script){
//Set the script for the target block.
string str = script->getString();
if (!str.empty())
newScript[gameObjectEventNameMap[script->name]] = str;
}
}
}
newId = block->id;
if (id){
newId = id->caption;
}
// Check achievement
if (!newScript.empty()) {
statsMgr.newAchievement("helloworld");
}
// now do the actual changes
commandManager->doCommand(new SetScriptCommand(this, block, newScript, newId));
}
}
else if (name == "cfgAddLayerOK") {
GUIObject* object = obj->getChild("layerName");
GUIObject* objLayerType = obj->getChild("layerType");
if (!object || !objLayerType) return;
if (object->caption.empty()) {
msgBox(imageManager, renderer, _("Please enter a layer name."), MsgBoxOKOnly, _("Error"));
return;
}
std::string layerName = (objLayerType->value ? "fg_" : "bg_") + object->caption;
if (sceneryLayers.find(layerName) != sceneryLayers.end()) {
msgBox(imageManager, renderer, tfm::format(
objLayerType->value ? _("There is already a foreground layer named '%s'.") : _("There is already a background layer named '%s'."),
object->caption), MsgBoxOKOnly, _("Error"));
return;
}
// do the actual operation
commandManager->doCommand(new AddRemoveLayerCommand(this, layerName, true));
}
else if (name == "cfgLayerSettingsOK") {
SetLayerPropertyCommand::LayerProperty prop;
GUIObject* object = obj->getChild("layerName");
GUIObject* objLayerType = obj->getChild("layerType");
if (!object || !objLayerType) return;
prop.name = (objLayerType->value ? "fg_" : "bg_") + object->caption;
object = obj->getChild("speedX");
if (!object) return;
prop.speedX = atof(object->caption.c_str());
object = obj->getChild("speedY");
if (!object) return;
prop.speedY = atof(object->caption.c_str());
object = obj->getChild("cameraX");
if (!object) return;
prop.cameraX = atof(object->caption.c_str());
object = obj->getChild("cameraY");
if (!object) return;
prop.cameraY = atof(object->caption.c_str());
object = obj->getChild("oldName");
if (!object) return;
const std::string& oldName = object->caption;
if (prop.name.size() <= 3) {
msgBox(imageManager, renderer, _("Please enter a layer name."), MsgBoxOKOnly, _("Error"));
return;
}
if (prop.name != oldName && sceneryLayers.find(prop.name) != sceneryLayers.end()) {
msgBox(imageManager, renderer, tfm::format(
objLayerType->value ? _("There is already a foreground layer named '%s'.") : _("There is already a background layer named '%s'."),
prop.name.substr(3)), MsgBoxOKOnly, _("Error"));
return;
}
// do the actual operation
commandManager->doCommand(new SetLayerPropertyCommand(this, oldName, prop));
}
else if (name == "cfgMoveToLayerOK") {
GUIObject* object = obj->getChild("layerName");
GUIObject* objLayerType = obj->getChild("layerType");
if (!object || !objLayerType) return;
const std::string& layerName = (objLayerType->value ? "fg_" : "bg_") + object->caption;
object = obj->getChild("oldName");
if (!object) return;
const std::string& oldName = object->caption;
if (layerName.size() <= 3) {
msgBox(imageManager, renderer, _("Please enter a layer name."), MsgBoxOKOnly, _("Error"));
return;
}
if (oldName == layerName) {
msgBox(imageManager, renderer, _("Source and destination layers are the same."), MsgBoxOKOnly, _("Error"));
return;
}
// do the actual operation
commandManager->doCommand(new MoveToLayerCommand(this, selection, oldName, layerName));
}
else if (name == "cfgCustomSceneryOK") {
//Get the configuredObject.
Scenery* configuredObject = dynamic_cast<Scenery*>(objectWindows[obj]);
if (configuredObject){
//Get the custom scenery from the GUIWindow.
GUITextArea* txt = (GUITextArea*)obj->getChild("cfgCustomScenery");
if (txt){
//Set the custom scenery.
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
configuredObject, "customScenery", txt->getString(), _("Scenery")));
}
}
}
else if (name == "lstAppearance") {
//Get the configuredObject.
Block *block = dynamic_cast<Block*>(objectWindows[obj]);
GUIListBox *list = dynamic_cast<GUIListBox*>(obj->getChild("lstAppearance"));
GUIImage *image = dynamic_cast<GUIImage*>(obj->getChild("imgAppearance"));
if (block && list && image) {
//Reset the image first.
image->setImage(NULL);
image->setClipRect(SDL_Rect{ 0, 0, 0, 0 });
//Get the appearance name.
std::string appearanceName;
if (list->value <= 0) {
//Do nothing since the selected is the default appearance.
} else if (list->value <= (int)sceneryBlockNames.size()) {
//A custom appearance is selected.
appearanceName = sceneryBlockNames[list->value - 1];
} else {
//The configured object has an invalid custom appearance name.
appearanceName = block->customAppearanceName;
}
//Try to find the theme block.
ThemeBlock *themeBlock = NULL;
if (!appearanceName.empty()) {
themeBlock = objThemes.getScenery(appearanceName);
}
if (themeBlock == NULL) {
themeBlock = objThemes.getBlock(block->type);
}
if (themeBlock) {
image->setImage(themeBlock->editorPicture.texture);
const auto& offsetData = themeBlock->editorPicture.offset.offsetData;
if (offsetData.size() > 0) {
const auto& r = offsetData[0];
image->setClipRect(SDL_Rect{ r.x, r.y, r.w, r.h });
}
}
}
return;
}
else if (name == "cfgAppearanceOK") {
//Get the configuredObject.
Block *block = dynamic_cast<Block*>(objectWindows[obj]);
GUIListBox *list = dynamic_cast<GUIListBox*>(obj->getChild("lstAppearance"));
if (block && list) {
//Get the appearance name.
std::string appearanceName;
if (list->value <= 0) {
//Do nothing since the selected is the default appearance.
} else if (list->value <= (int)sceneryBlockNames.size()) {
//A custom appearance is selected.
appearanceName = sceneryBlockNames[list->value - 1];
} else {
//The configured object has an invalid custom appearance name.
appearanceName = block->customAppearanceName;
}
//Update the block property if it's changed.
if (appearanceName != block->customAppearanceName) {
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
block, "appearance", appearanceName, _("Appearance")));
}
}
}
//NOTE: We assume every event came from a window
//and the event is either window closed event or OK/Cancel button click event, so we remove it.
destroyWindow(obj);
}
void LevelEditor::destroyWindow(GUIObject* window){
//Make sure the given pointer isn't null.
if(!window)
return;
//Remove the window from the GUIObject root.
if(GUIObjectRoot){
vector<GUIObject*>::iterator it;
it=find(GUIObjectRoot->childControls.begin(),GUIObjectRoot->childControls.end(),window);
if(it!=GUIObjectRoot->childControls.end()){
GUIObjectRoot->childControls.erase(it);
}
}
//Also remove the window from the objectWindows map.
map<GUIObject*,GameObject*>::iterator it;
it=objectWindows.find(window);
if(it!=objectWindows.end()){
objectWindows.erase(it);
}
//And delete the GUIWindow.
delete window;
}
////////////////LOGIC////////////////////
void LevelEditor::logic(ImageManager& imageManager, SDL_Renderer& renderer){
if(playMode){
//PlayMode so let the game do it's logic.
Game::logic(imageManager,renderer);
}else{
//Update animation even under edit mode. (There are checks in Block::move() which don't do game logic in edit mode.)
for (unsigned int i = 0; i<levelObjects.size(); i++){
//Let the gameobject handle movement.
levelObjects[i]->move();
}
//Also update the scenery.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it){
it->second->updateAnimation();
}
//In case of a selection or actions popup prevent the camera from moving.
if(selectionPopup || actionsPopup)
return;
//Move the camera.
if (cameraXvel != 0 || cameraYvel != 0) {
if (pressedShift) {
if (cameraXvel > 0) cameraXvel++;
else if (cameraXvel < 0) cameraXvel--;
if (cameraYvel > 0) cameraYvel++;
else if (cameraYvel < 0) cameraYvel--;
}
camera.x = clamp(camera.x + cameraXvel, -1000 - SCREEN_WIDTH, levelRect.w + 1000);
camera.y = clamp(camera.y + cameraYvel, -1000 - SCREEN_HEIGHT, levelRect.h + 1000);
//Call the onCameraMove event.
onCameraMove(cameraXvel, cameraYvel);
}
//Move the camera with the mouse.
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
{
//Check if the mouse isn't above a GUIObject (window).
bool inside=false;
for(unsigned int i=0;i<GUIObjectRoot->childControls.size();i++){
SDL_Rect box={0,0,0,0};
box.x=GUIObjectRoot->childControls[i]->left;
box.y=GUIObjectRoot->childControls[i]->top;
box.w=GUIObjectRoot->childControls[i]->width;
box.h=GUIObjectRoot->childControls[i]->height;
if(pointOnRect(mouse,box))
inside=true;
}
if(!inside){
SDL_Rect r[3]={toolbarRect,toolboxRect};
int m=2;
//TODO: Also call onCameraMove when moving using the mouse.
setCamera(r,m);
}
}
//It isn't playMode so the mouse should be checked.
tooltip=-1;
//We loop through the number of tools + the number of buttons.
for (int t = 0; t <= (int)ToolTips::BackToMenu; t++){
SDL_Rect toolRect={(SCREEN_WIDTH-460)/2+(t*40)+((t+1)*10),SCREEN_HEIGHT-45,40,40};
//Check for collision.
if(pointOnRect(mouse,toolRect)==true){
//Set the tooltip tool.
tooltip=t;
}
}
}
}
/////////////////RENDER//////////////////////
void LevelEditor::render(ImageManager& imageManager,SDL_Renderer& renderer){
//Let the game render the game when it is the play mode.
if (playMode) {
Game::render(imageManager, renderer);
} else {
// The following code are partially copied from Game::render()
//First of all render the background.
{
//Get a pointer to the background.
ThemeBackground* bg = background;
//Check if the background is null, but there are themes.
if (bg == NULL && objThemes.themeCount()>0){
//Get the background from the first theme in the stack.
bg = objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
if (bg){
//It isn't so draw it.
bg->draw(renderer);
//And if it's the loaded background then also update the animation.
//FIXME: Updating the animation in the render method?
if (bg == background)
bg->updateAnimation();
} else{
//There's no background so fill the screen with white.
SDL_SetRenderDrawColor(&renderer, 255, 255, 255, 255);
SDL_RenderClear(&renderer);
}
}
//Now draw the background layers.
auto it = sceneryLayers.begin();
for (; it != sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
if (layerVisibility[it->first]) {
it->second->show(renderer);
}
}
//Now we draw the levelObjects.
if (layerVisibility[std::string()]) {
//NEW: always render the pushable blocks in front of other blocks
std::vector<Block*> pushableBlocks;
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->show(renderer);
}
}
for (auto o : pushableBlocks) {
o->show(renderer);
}
}
//We don't draw the player and the shadow at all.
//Now draw the foreground layers.
for (; it != sceneryLayers.end(); ++it){
if (layerVisibility[it->first]) {
it->second->show(renderer);
}
}
}
//Only render extra stuff like the toolbar, selection, etc.. when not in playMode.
if(!playMode){
//Get the current mouse location.
int x, y;
SDL_GetMouseState(&x, &y);
//Create the rectangle.
SDL_Rect mouse = { x + camera.x, y + camera.y, 0, 0 };
//Render the selectionmarks.
//TODO: Check if block is in sight.
for(unsigned int o=0; o<selection.size(); o++){
//Get the location to draw.
SDL_Rect r=selection[o]->getBox();
// Change the mouse cursor if necessary
if (selectionDrag < 0 && tool != REMOVE) {
int midx = r.x + r.w / 2 - 2;
int midy = r.y + r.h / 2 - 2;
if (mouse.x >= r.x && mouse.x < r.x + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
currentCursor = CURSOR_SIZE_FDIAG;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
currentCursor = CURSOR_SIZE_HOR;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
currentCursor = CURSOR_SIZE_BDIAG;
}
} else if (mouse.x >= midx && mouse.x < midx + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
currentCursor = CURSOR_SIZE_VER;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
currentCursor = CURSOR_SIZE_VER;
}
} else if (mouse.x >= r.x + r.w - 5 && mouse.x < r.x + r.w) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
currentCursor = CURSOR_SIZE_BDIAG;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
currentCursor = CURSOR_SIZE_HOR;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
currentCursor = CURSOR_SIZE_FDIAG;
}
}
}
bool mouseIn = pointOnRect(mouse, r);
r.x-=camera.x;
r.y-=camera.y;
drawGUIBox(r.x,r.y,r.w,r.h,renderer,0xFFFFFF33);
//Draw the selectionMarks.
applyTexture(r.x,r.y,selectionMark,renderer);
applyTexture(r.x+r.w-5,r.y,selectionMark,renderer);
applyTexture(r.x,r.y+r.h-5,selectionMark,renderer);
applyTexture(r.x+r.w-5,r.y+r.h-5,selectionMark,renderer);
// draw additional selection marks
if (mouseIn && selectionDrag < 0 && tool != REMOVE) {
applyTexture(r.x + r.w / 2 - 2, r.y, selectionMark, renderer);
applyTexture(r.x + r.w / 2 - 2, r.y + r.h - 5, selectionMark, renderer);
applyTexture(r.x, r.y + r.h / 2 - 2, selectionMark, renderer);
applyTexture(r.x + r.w - 5, r.y + r.h / 2 - 2, selectionMark, renderer);
}
}
//Set the color for the borders.
{
SDL_Color c = objThemes.getTextColor(false);
SDL_SetRenderDrawColor(&renderer, c.r, c.g, c.b, 115);
}
int leftWidth=0;
int rightWidth=0;
//Draw the dark areas marking the outside of the level.
SDL_Rect r{0,0,0,0};
if(camera.x<0){
//Draw left side.
r.x=0;
r.y=0;
r.w=0-camera.x;
leftWidth=r.w;
r.h=SCREEN_HEIGHT;
SDL_RenderFillRect(&renderer, &r);
}
if(camera.y<0){
//Draw the top.
r.x=leftWidth;
r.y=0;
r.w=SCREEN_WIDTH;
r.h=0-camera.y;
SDL_RenderFillRect(&renderer, &r);
} else {
r.h=0;
}
if(camera.x>levelRect.w-SCREEN_WIDTH){
//Draw right side.
r.x=levelRect.w-camera.x;
r.y=std::max(r.y+r.h,0);
r.w=SCREEN_WIDTH-(levelRect.w-camera.x);
rightWidth=r.w;
r.h=SCREEN_HEIGHT;
SDL_RenderFillRect(&renderer, &r);
}
if(camera.y>levelRect.h-SCREEN_HEIGHT){
//Draw the bottom.
r.x=leftWidth;
r.y=levelRect.h-camera.y;
r.w=SCREEN_WIDTH-rightWidth-leftWidth;
r.h=SCREEN_HEIGHT-(levelRect.h-camera.y);
SDL_RenderFillRect(&renderer, &r);
}
//Check if we should draw on stuff.
showConfigure(renderer);
if (selectionDrag >= 0 && tool != REMOVE) {
showSelectionDrag(renderer);
}
//Find a block where the mouse is hovering on.
bool isMouseOnSomething = false;
if (selectedLayer.empty()){
if (layerVisibility[selectedLayer]) {
// Current layer is Blocks layer
for (unsigned int o = 0; o<levelObjects.size(); o++){
SDL_Rect rect = levelObjects[o]->getBox();
if (pointOnRect(mouse, rect) == true){
isMouseOnSomething = true;
if (tool == REMOVE){
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFF000055);
currentCursor = CURSOR_REMOVE;
} else{
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFFFFFF33);
}
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
// Current layer is scenery layer
for (auto o : it->second->objects){
SDL_Rect rect = o->getBox();
if (pointOnRect(mouse, rect) == true){
isMouseOnSomething = true;
if (tool == REMOVE){
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFF000055);
currentCursor = CURSOR_REMOVE;
} else{
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFFFFFF33);
}
}
}
}
}
// show current object only when mouse is not hover on any blocks
if (!isMouseOnSomething && tool == ADD && selectionDrag < 0) {
showCurrentObject(renderer);
}
//Draw the level borders.
drawRect(-camera.x,-camera.y,levelRect.w,levelRect.h,renderer);
//Render the hud layer.
renderHUD(renderer);
//Render selection popup (if any).
if(selectionPopup!=NULL){
if(linking || moving){
//If we switch to linking mode then delete it
//FIXME: Logic in the render method.
delete selectionPopup;
selectionPopup=NULL;
}else{
selectionPopup->render(imageManager,renderer);
}
}
//Render actions popup (if any).
if(actionsPopup!=NULL){
actionsPopup->render(renderer);
}
}
}
void LevelEditor::renderHUD(SDL_Renderer& renderer){
//If moving show the moving speed in the top right corner.
if(moving){
//Calculate width of text "Movespeed: 125" to keep the same position with every value
if (movingSpeedWidth == -1){
int w;
TTF_SizeUTF8(fontText, tfm::format(_("Speed: %d = %0.2f block/s"), 125, 10.0f).c_str(), &w, NULL);
movingSpeedWidth = w + 4;
}
SDL_Texture *tex = NULL;
//Check which text should we use.
if (pauseMode) {
//Update the text if necessary.
if (pauseTimeTexture.needsUpdate(pauseTime)) {
if (pauseTime < 0) {
pauseTimeTexture.update(pauseTime,
textureFromText(renderer, *fontText,
_("Stop at this point"),
objThemes.getTextColor(true)));
} else {
pauseTimeTexture.update(pauseTime,
textureFromText(renderer, *fontText,
tfm::format(_("Pause: %d = %0.3fs"), pauseTime, float(pauseTime)*0.025f).c_str(),
objThemes.getTextColor(true)));
}
}
tex = pauseTimeTexture.get();
} else {
//Update the text if necessary.
if (movementSpeedTexture.needsUpdate(movingSpeed)) {
movementSpeedTexture.update(movingSpeed,
textureFromText(renderer, *fontText,
tfm::format(_("Speed: %d = %0.2f block/s"), movingSpeed, float(movingSpeed)*0.08f).c_str(),
objThemes.getTextColor(true)));
}
tex = movementSpeedTexture.get();
}
//Draw the text in the box.
drawGUIBox(SCREEN_WIDTH-movingSpeedWidth-2,-2,movingSpeedWidth+8,
textureHeight(*tex)+6,renderer,0xFFFFFFFF);
applyTexture(SCREEN_WIDTH-movingSpeedWidth,2,*tex,renderer,NULL);
}
//On top of all render the toolbar.
drawGUIBox(toolbarRect.x,toolbarRect.y,9*50+10,52,renderer,0xEDEDEDFF);
//Draw the first four options.
SDL_Rect srcRect={0,0,200,50};
SDL_Rect dstRect={toolbarRect.x+5, toolbarRect.y, srcRect.w, srcRect.h};
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
//Draw the undo/redo button.
SDL_SetTextureAlphaMod(toolbar.get(), commandManager->canUndo() ? 255 : 128);
srcRect.x = 200;
srcRect.w = 50;
dstRect.x = toolbarRect.x + 205;
dstRect.w = srcRect.w;
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
SDL_SetTextureAlphaMod(toolbar.get(), commandManager->canRedo() ? 255 : 128);
srcRect.x = 250;
srcRect.w = 50;
dstRect.x = toolbarRect.x + 255;
dstRect.w = srcRect.w;
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
SDL_SetTextureAlphaMod(toolbar.get(), 255);
//And the last three.
srcRect.x=300;
srcRect.w=150;
dstRect.x=toolbarRect.x+305;
dstRect.w=srcRect.w;
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
//Now render a tooltip.
if(tooltip>=0 && static_cast<std::size_t>(tooltip)<tooltipTextures.size()) {
SDL_Texture *tex = tooltipTextures.at(tooltip).get();
if (tooltip == (int)ToolTips::UndoNoTooltip) {
std::string s = commandManager->describeUndo();
if (undoTooltipTexture.needsUpdate(s)) {
undoTooltipTexture.update(s, textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true)));
}
tex = undoTooltipTexture.get();
} else if (tooltip == (int)ToolTips::RedoNoTooltip) {
std::string s = commandManager->describeRedo();
if (redoTooltipTexture.needsUpdate(s)) {
redoTooltipTexture.update(s, textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true)));
}
tex = redoTooltipTexture.get();
}
if(tex) {
const SDL_Rect texSize = rectFromTexture(*tex);
SDL_Rect r={(SCREEN_WIDTH-440)/2+(tooltip*40)+(tooltip*10),SCREEN_HEIGHT-45,40,40};
r.y=SCREEN_HEIGHT-50-texSize.h;
if(r.x+texSize.w>SCREEN_WIDTH-50)
r.x=SCREEN_WIDTH-50-texSize.w;
//Draw borders around text
Uint32 color=0xFFFFFF00|230;
drawGUIBox(r.x-2,r.y-2,texSize.w+4,texSize.h+4,renderer,color);
applyTexture(r.x, r.y, *tex, renderer);
}
}
// for toolbox button animation (0-31)
static int tick = 8;
const int mmm = getEditorOrderMax();
if (currentType >= mmm)currentType = mmm - 1;
if (currentType < 0) currentType = 0;
//Render the tool box.
if(!playMode && !moving && tool==ADD && selectionPopup==NULL && actionsPopup==NULL && objectWindows.empty()){
// get mouse position
int x, y;
SDL_GetMouseState(&x, &y);
if (toolboxVisible){
toolboxRect.x=0;
toolboxRect.y=0;
toolboxRect.w=SCREEN_WIDTH;
toolboxRect.h=64;
drawGUIBox(-2,-2,SCREEN_WIDTH+4,66,renderer,0xFFFFFF00|230);
bool isMouseOnSomething = false;
//Draw the hide icon.
SDL_Rect r={SCREEN_WIDTH-20,2,16,16};
SDL_Rect r2={80,0,r.w,r.h};
if (x >= SCREEN_WIDTH - 24 && x < SCREEN_WIDTH && y < 20) {
isMouseOnSomething = true;
tick = (tick + 1) & 31;
r.y -= (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
}
SDL_RenderCopy(&renderer, bmGUI.get(), &r2, &r);
//Calculate the maximal number of blocks can be displayed.
const int m=(SCREEN_WIDTH-48)/64;
if(toolboxIndex>=mmm-m){
toolboxIndex = mmm - m;
}else{
//Draw an icon.
r.x=SCREEN_WIDTH-20;
r.y=24;
r2.x=96;
r2.y=16;
if (x >= SCREEN_WIDTH - 24 && x < SCREEN_WIDTH && y >= 20 && y < 44) {
isMouseOnSomething = true;
tick = (tick + 1) & 31;
r.x += (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
}
SDL_RenderCopy(&renderer, bmGUI.get(),&r2,&r);
}
if(toolboxIndex<=0){
toolboxIndex=0;
}else{
//Draw an icon.
r.x=4;
r.y=24;
r2.x=80;
r2.y=16;
if (x >= 0 && x < 24 && y >= 20 && y < 44) {
isMouseOnSomething = true;
tick = (tick + 1) & 31;
r.x -= (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
}
SDL_RenderCopy(&renderer, bmGUI.get(), &r2, &r);
}
// reset animation timer if there is no animation
if (!isMouseOnSomething) {
tick = 8;
}
//Draw available blocks.
for(int i=0;i<m;i++){
if (i + toolboxIndex >= mmm) break;
//Draw a rectangle around the current tool.
if(i+toolboxIndex==currentType){
drawGUIBox(i*64+24,3,64,58,renderer,0xDDDDDDFF);
}
if (selectedLayer.empty()) {
// show normal blocks
ThemeBlock* obj = objThemes.getBlock(editorTileOrder[i + toolboxIndex]);
if (obj){
obj->editorPicture.draw(renderer, i * 64 + 24 + 7, 7);
}
} else {
// show scenery blocks
if (i + toolboxIndex < (int)sceneryBlockNames.size()) {
ThemeBlock* obj = objThemes.getScenery(sceneryBlockNames[i + toolboxIndex]);
if (obj){
obj->editorPicture.draw(renderer, i * 64 + 24 + 7, 7);
}
} else {
// it's custom scenery block
// just draw a stupid icon
const SDL_Rect r = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { i * 64 + 24 + 7, 7, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
}
}
}
//Draw a tool tip.
if(y<64 && x>=24 && x<24+m*64){
int i=(x-24)/64;
if (i + toolboxIndex < getEditorOrderMax()){
TexturePtr& tip = (!selectedLayer.empty())
? getCachedTextTexture(renderer, (i + toolboxIndex < (int)sceneryBlockNames.size())
? describeSceneryName(sceneryBlockNames[i + toolboxIndex]).c_str() : _("Custom scenery block"))
: typeTextTextures.at(editorTileOrder[i + toolboxIndex]);
const SDL_Rect tipSize = rectFromTexture(*tip);
SDL_Rect r = { 24 + i * 64, 64, 40, 40 };
if (r.x + tipSize.w>SCREEN_WIDTH - 50)
r.x = SCREEN_WIDTH - 50 - tipSize.w;
//Draw borders around text
Uint32 color = 0xFFFFFF00 | 230;
drawGUIBox(r.x - 2, r.y - 2, tipSize.w + 4, tipSize.h + 4, renderer, color);
//Draw tooltip's text
applyTexture(r.x, r.y, tip, renderer);
}
}
}else{
const SDL_Rect tbtSize = rectFromTexture(*toolboxText);
toolboxRect.x=SCREEN_WIDTH-tbtSize.w-28;
toolboxRect.y=0;
toolboxRect.w=tbtSize.w+28;
toolboxRect.h=tbtSize.h+4;
SDL_Rect r={SCREEN_WIDTH-tbtSize.w-24,2,16,16};
drawGUIBox(r.x-4,-2,tbtSize.w+32,tbtSize.h+6,renderer,0xFFFFFFFF);
//Draw "Toolbox" text.
applyTexture(r.x, r.y, toolboxText, renderer);
const SDL_Rect r2={96,0,16,16};
r.x=SCREEN_WIDTH-20;
r.w=r2.w;
r.h=r2.h;
// check if mouse is hovering on
if (x >= toolboxRect.x && x < toolboxRect.x + toolboxRect.w && y >= toolboxRect.y && y < toolboxRect.y + toolboxRect.h) {
tick = (tick + 1) & 31;
r.y += (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
} else {
tick = 8;
}
//Draw arrow.
SDL_RenderCopy(&renderer, bmGUI.get(),&r2,&r);
}
}else{
toolboxRect.x=-1;
toolboxRect.y=-1;
toolboxRect.w=0;
toolboxRect.h=0;
}
//Draw a rectangle around the current tool.
Uint32 color=0xFFFFFF00;
drawGUIBox((SCREEN_WIDTH-440)/2+(tool*40)+(tool*10),SCREEN_HEIGHT-46,42,42,renderer,color);
}
void LevelEditor::showCurrentObject(SDL_Renderer& renderer){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
x+=camera.x;
y+=camera.y;
//Check if we should snap the block to grid or not.
if(!pressedShift){
snapToGrid(&x,&y);
}else{
x-=25;
y-=25;
}
//Check if the currentType is a legal type.
if(currentType>=0 && currentType<getEditorOrderMax()){
if (selectedLayer.empty()) {
// show normal blocks
ThemeBlock* obj = objThemes.getBlock(editorTileOrder[currentType]);
if (obj){
obj->editorPicture.draw(renderer, x - camera.x, y - camera.y);
}
} else {
// show scenery blocks
if (currentType < (int)sceneryBlockNames.size()) {
ThemeBlock* obj = objThemes.getScenery(sceneryBlockNames[currentType]);
if (obj){
obj->editorPicture.draw(renderer, x - camera.x, y - camera.y);
}
} else {
// it's custom scenery block
// just draw a stupid icon
const SDL_Rect r = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { x - camera.x, y - camera.y, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
}
}
}
}
void LevelEditor::determineNewPosition(int& x, int& y) {
if (dragCenter) {
SDL_Rect r = dragCenter->getBox();
x -= dragSrartPosition.x - r.x;
y -= dragSrartPosition.y - r.y;
} else {
x -= 25;
y -= 25;
}
// Check if we should snap the block to grid or not.
if (!pressedShift) {
x = int(floor(x/50.0f + 0.5f)) * 50;
y = int(floor(y/50.0f + 0.5f)) * 50;
}
}
void LevelEditor::determineNewSize(int x, int y, SDL_Rect& r) {
switch (selectionDrag % 3) {
case 0:
if (x > r.x + r.w - 15) x = r.x + r.w - 15;
if (!pressedShift) {
x = int(floor(x/50.0f + 0.5f)) * 50;
while (x > r.x + r.w - 15) x -= 50;
}
r.w += r.x - x;
r.x = x;
break;
case 2:
if (x < r.x + 15) x = r.x + 15;
if (!pressedShift) {
x = int(floor(x/50.0f + 0.5f)) * 50;
while (x < r.x + 15) x += 50;
}
r.w = x - r.x;
break;
}
switch (selectionDrag / 3) {
case 0:
if (y > r.y + r.h - 15) y = r.y + r.h - 15;
if (!pressedShift) {
y = int(floor(y/50.0f + 0.5f)) * 50;
while (y > r.y + r.h - 15) y -= 50;
}
r.h += r.y - y;
r.y = y;
break;
case 2:
if (y < r.y + 15) y = r.y + 15;
if (!pressedShift) {
y = int(floor(y/50.0f + 0.5f)) * 50;
while (y < r.y + 15) y += 50;
}
r.h = y - r.y;
break;
}
}
void LevelEditor::showSelectionDrag(SDL_Renderer& renderer){
//Check if the drag center isn't null.
if (dragCenter == NULL) return;
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
x+=camera.x;
y+=camera.y;
//The location of the dragCenter.
SDL_Rect r = dragCenter->getBox();
if (selectionDrag == 4) { // dragging
// Check if we should snap the block to grid or not.
determineNewPosition(x, y);
//Loop through the selection.
//TODO: Check if block is in sight.
for (unsigned int o = 0; o < selection.size(); o++){
// FIXME: ad-hoc code which moves blocks temporarily, draw, and moves them back
const SDL_Rect r1 = selection[o]->getBox();
selection[o]->setBaseLocation((r1.x - r.x) + x, (r1.y - r.y) + y);
selection[o]->show(renderer);
selection[o]->setBaseLocation(r1.x, r1.y);
}
} else if (selectionDrag >= 0) { // resizing
// Check if we should snap the block to grid or not.
determineNewSize(x, y, r);
drawGUIBox(r.x - camera.x, r.y - camera.y, r.w, r.h, renderer, 0xFFFFFF33);
}
}
void LevelEditor::showConfigure(SDL_Renderer& renderer){
//arrow animation value. go through 0-65535 and loops.
static unsigned short arrowAnimation=0;
arrowAnimation++;
// skip if the Blocks layer is invisinble
if (!layerVisibility[std::string()]) return;
//Use theme color for arrows.
Uint32 color;
{
SDL_Color c = objThemes.getTextColor(false);
color = (Uint32(c.r) << 24) | (Uint32(c.g) << 16) | (Uint32(c.b) << 8) | 0xff;
}
//Draw the trigger lines.
{
map<Block*,vector<GameObject*> >::iterator it;
for(it=triggers.begin();it!=triggers.end();++it){
//Check if the trigger has linked targets.
if(!(*it).second.empty()){
//The location of the trigger.
SDL_Rect r=(*it).first->getBox();
//Loop through the targets.
for(unsigned int o=0;o<(*it).second.size();o++){
//Get the location of the target.
SDL_Rect r1=(*it).second[o]->getBox();
//Draw the line from the center of the trigger to the center of the target.
drawLineWithArrow(r.x-camera.x+25,r.y-camera.y+25,r1.x-camera.x+25,r1.y-camera.y+25,renderer,color,32,arrowAnimation%32);
//Also draw two selection marks.
applyTexture(r.x-camera.x+25-2,r.y-camera.y+25-2,selectionMark,renderer);
applyTexture(r1.x-camera.x+25-2,r1.y-camera.y+25-2,selectionMark,renderer);
}
}
}
//Draw a line to the mouse from the linkingTrigger when linking.
if(linking){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Draw the line from the center of the trigger to mouse.
drawLineWithArrow(linkingTrigger->getBox().x-camera.x+25,linkingTrigger->getBox().y-camera.y+25,x,y,renderer,color,32,arrowAnimation%32);
}
}
//This saves the stacked pause marks on each position.
std::map<std::pair<int, int>, int> stackedMarks;
//Draw the moving positions.
map<Block*,vector<MovingPosition> >::iterator it;
for(it=movingBlocks.begin();it!=movingBlocks.end();++it){
//Check if the block has positions.
if(!(*it).second.empty()){
//The location of the moving block.
SDL_Rect block=(*it).first->getBox();
block.x+=25-camera.x;
block.y+=25-camera.y;
//The location of the previous position.
//The first time it's the moving block's position self.
SDL_Rect r=block;
//Loop through the positions.
for(unsigned int o=0;o<(*it).second.size();o++){
//Draw the line from the center of the previous position to the center of the position.
//x and y are the coordinates for the current moving position.
int x=block.x+(*it).second[o].x;
int y=block.y+(*it).second[o].y;
//Check if we need to draw line
double dx=r.x-x;
double dy=r.y-y;
double d=sqrt(dx*dx+dy*dy);
if(d>0.001f){
if(it->second[o].time>0){
//Calculate offset to contain the moving speed.
int offset=int(d*arrowAnimation/it->second[o].time)%32;
drawLineWithArrow(r.x,r.y,x,y,renderer,color,32,offset);
}else{
//time==0 ???? so don't draw arrow at all
drawLine(r.x,r.y,x,y,renderer);
}
} else {
// distance==0 which means pause mode
// FIXME: it's ugly
SDL_Rect r1 = { 0, 0, 16, 16 }, r2 = { x - 25, y - 25 + 15 * (stackedMarks[std::pair<int, int>(x, y)]++), 16, 16 };
if (it->second[o].time) {
char s[64];
sprintf(s, "%gs", float(it->second[o].time) * 0.025f);
r1.x = 0; r1.y = 80;
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r2);
r2.x += 16;
TexturePtr &tex = getSmallCachedTextTexture(renderer, s);
SDL_Rect r = rectFromTexture(tex);
// background
if (r.w <= 8) {
r1.x = 32 - r.w; r1.w = r.w;
r2.w = r.w;
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r2);
} else {
r1.x = 16; r1.w = 8;
r2.w = r.w - 8;
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r2);
r1.x = 24;
SDL_Rect r3 = { r2.x + r.w - 8, r2.y, 8, r2.h };
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r3);
}
// text
applyTexture(r2.x - 2, r2.y + (16 - r.h) / 2, tex, renderer);
} else {
r1.x = 32; r1.y = 64;
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r2);
}
}
//And draw a marker at the end.
applyTexture(x-13,y-13,movingMark,renderer);
//Get the box of the previous position.
SDL_Rect tmp={x,y,0,0};
r=tmp;
}
}
}
//Draw a line to the mouse from the previous moving pos.
if(moving){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Check if we should snap the block to grid or not.
if(!pressedShift){
x+=camera.x;
y+=camera.y;
snapToGrid(&x,&y);
x-=camera.x;
y-=camera.y;
}else{
x-=25;
y-=25;
}
int posX,posY;
//Check if there are moving positions for the moving block.
if(!movingBlocks[movingBlock].empty()){
//Draw the line from the center of the previouse moving positions to mouse.
posX=movingBlocks[movingBlock].back().x;
posY=movingBlocks[movingBlock].back().y;
posX-=camera.x;
posY-=camera.y;
posX+=movingBlock->getBox().x;
posY+=movingBlock->getBox().y;
}else{
//Draw the line from the center of the movingblock to mouse.
posX=movingBlock->getBox().x-camera.x;
posY=movingBlock->getBox().y-camera.y;
}
//Check if the current point is the same as the previous point
if (posX == x && posY == y) {
pauseMode = true;
} else {
pauseMode = false;
//Calculate offset to contain the moving speed.
int offset = int(double(arrowAnimation)*movingSpeed / 10.0) % 32;
//Draw the line.
drawLineWithArrow(posX + 25, posY + 25, x + 25, y + 25, renderer, color, 32, offset);
}
//Draw a marker.
applyTexture(x+12,y+12,movingMark,renderer);
}
}
void LevelEditor::resize(ImageManager &imageManager, SDL_Renderer &renderer){
//Call the resize method of the Game.
Game::resize(imageManager, renderer);
//Move the toolbar's position rect used for collision.
toolbarRect.x=(SCREEN_WIDTH-460)/2;
toolbarRect.y=SCREEN_HEIGHT-50;
}
//Filling the order array
const int LevelEditor::editorTileOrder[EDITOR_ORDER_MAX]={
TYPE_BLOCK,
TYPE_SHADOW_BLOCK,
TYPE_SPIKES,
TYPE_FRAGILE,
TYPE_MOVING_BLOCK,
TYPE_MOVING_SHADOW_BLOCK,
TYPE_MOVING_SPIKES,
TYPE_CONVEYOR_BELT,
TYPE_SHADOW_CONVEYOR_BELT,
TYPE_BUTTON,
TYPE_SWITCH,
TYPE_PORTAL,
TYPE_SWAP,
TYPE_CHECKPOINT,
TYPE_NOTIFICATION_BLOCK,
TYPE_START_PLAYER,
TYPE_START_SHADOW,
TYPE_EXIT,
TYPE_COLLECTABLE,
TYPE_PUSHABLE
};
diff --git a/src/LevelPack.h b/src/LevelPack.h
index d9263e5..9ef58ac 100644
--- a/src/LevelPack.h
+++ b/src/LevelPack.h
@@ -1,242 +1,241 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LEVELPACK_H
#define LEVELPACK_H
#include <vector>
#include <string>
#include "libs/tinygettext/tinygettext.hpp"
enum LevelPackType{
//Main levelpacks are distibuted along with the game and located in the data path.
MAIN,
//Addon levelpacks are downloaded/added packs which reside in the user data path.
ADDON,
//Custom levelpacks are user made and are located in the
CUSTOM,
//Collection levelpacks can contain levels from different locations.
//This type is used for the Levels and Custom Levels levelpacks.
//NOTE: The levelpackPath is ignored for these type of levelpacks since levels can be anywhere.
COLLECTION
};
class LevelPack{
public:
//A level entry structure.
struct Level{
//The name of the level.
std::string name;
//The filename of the level.
std::string file;
//Boolean if the level is locked.
bool locked;
//Boolean if the level is won.
bool won;
//Boolean if the level is arcade.
bool arcade;
//Integer containing the number of ticks (40 = 1s) it took to finish the level.
//If there's no time the value will be -1.
int time;
//Integer containing the target time to get a medal.
int targetTime;
//Integer containing the number of recordings used to finish the level.
//If the arcade mode is true, this means the number of collectibles instead.
//When not won the value is -1.
int recordings;
//Integer containing the target recordings to get a medal.
//If the arcade mode is true, this means the target number of collectibles instead.
int targetRecordings;
//MD5 of level node. :/
unsigned char md5Digest[16];
//MD5 in the level progress file.
unsigned char md5InLevelProgress[16];
//Get the medal of current level based on the time/targetTime/recordings/targetRecordings etc of this level.
//Return value: 0=no medal, 1=bronze medal, 2=silver medal, 3=gold medal
int getMedal() const {
if (won) return getMedal(arcade, time, targetTime, recordings, targetRecordings);
else return 0;
}
//Get the medal of current level, providing own time/recordings and assuming "won" is true.
//Return value: 1=bronze medal, 2=silver medal, 3=gold medal
int getMedal(int time, int recordings) const {
return getMedal(arcade, time, targetTime, recordings, targetRecordings);
}
//A generic function to get the modal of current level, assuming "won" is true.
//Return value: 1=bronze medal, 2=silver medal, 3=gold medal
static int getMedal(bool arcade, int time, int targetTime, int recordings, int targetRecordings);
//Get the better time selected from existing best time and the new time.
int getBetterTime(int newTime) const;
//Get the better recordings selected from existing best recordings and the new recordings.
int getBetterRecordings(int newRecordings) const;
};
private:
//Index of the current level.
int currentLevel;
//Boolean if the levels are loaded.
bool loaded;
//Vector containing the filenames of the levels.
std::vector<Level> levels;
//The file name of the level progress.
std::string levelProgressFile;
public:
//The name of the levelpack.
std::string levelpackName;
//The location the levelpack is stored.
std::string levelpackPath;
//A description of the levelpack.
std::string levelpackDescription;
//The type of levelpack.
LevelPackType type;
//The text that will be displayed when the levels are finished.
std::string congratulationText;
//The preferred music list to be used with this levelpack.
std::string levelpackMusicList;
//The dictionaryManager of the levelpack, used to translate strings.
tinygettext::DictionaryManager* dictionaryManager;
//Boolean if the levelpack has a custom theme/partial theme bundled with it.
//NOTE: Themes can't be preloaded since they would be destroyed by the ThemeStack.
bool customTheme;
//Constructor.
LevelPack();
//Destructor.
~LevelPack();
//gettext function
inline tinygettext::DictionaryManager* getDictionaryManager() const{
return dictionaryManager;
}
//Method for updating the language to the configured one.
//NOTE: This is called when changing the translation in the Options menu.
void updateLanguage();
//Adds a level to the levels.
//levelFileName: The filename of the level to add.
//level: The index of the level to add.
void addLevel(const std::string& levelFileName,int levelno=-1);
//Removes a level from the levels.
//level: The index of the level to remove.
void removeLevel(unsigned int level);
//Moves the level to a given index.
//level1: The level to move.
//level2: The destination.
void moveLevel(unsigned int level1,unsigned int level2);
//Swaps two level.
//level1: The first level to swap.
//level2: The second level to swap.
void swapLevel(unsigned int level1,unsigned int level2);
//Get the levelFile for a given level.
//level: The level index to get the levelFile from.
//Returns: String containing the levelFileName (full path to the file).
const std::string getLevelFile(int level=-1);
//Get the levelpackPath of the levels.
//Returns: String containing the levelpackPath.
const std::string& getLevelpackPath();
//Get the levelName for a given level.
//level: The level index to get the levelName from.
//Returns: String containing the levelName.
const std::string& getLevelName(int level=-1);
//Sets the levelName for a given level.
//level: The level index to get the levelName from.
//name: The new name of the level.
void setLevelName(unsigned int level,const std::string& name);
//Get the MD5 for a given level.
//level: The level index.
//Returns: const unsigned char[16] containing the digest.
const unsigned char* getLevelMD5(int level=-1);
//get level's auto-save record path,
//using level's MD5, file name and other information.
void getLevelAutoSaveRecordPath(int level,std::string &bestTimeFilePath,std::string &bestRecordingFilePath,bool createPath);
//get levelpack's auto-save record path, ends with '/'.
std::string getLevelpackAutoSaveRecordPath(bool createPath);
//get level's auto-save record file prefix (without path), using level's file name.
std::string getLevelAutoSaveRecordPrefix(int level);
//Method for getting the path to the progress file.
//Returns: The path + filename to the progress file.
std::string getLevelProgressPath();
//Set the currentLevel.
//level: The new current level.
void setCurrentLevel(unsigned int level);
//Get the currentLevel.
//Returns: The currentLevel.
inline int getCurrentLevel(){return currentLevel;}
//Get the levelCount.
//Returns: The level count.
inline int getLevelCount(){return levels.size();}
//Method that will return the requested level.
//level: The index of the level, default is the current level.
//Returns: Pointer to the requested level structure.
struct Level* getLevel(int level=-1);
//Method that will reset any progress/statistics for a given level.
//level: The index of the level to reset, default is currentLevel.
void resetLevel(int level=-1);
//Check if a certain level is locked.
//level: The index of the level to check.
//Returns: True if the level is locked.
bool getLocked(unsigned int level);
//Set a level locked or not.
//level: The level to (un)lock.
//locked: The new status of the level, default is unlocked (false).
void setLocked(unsigned int level,bool locked=false);
//Empties the levels.
void clear();
-
-
+
bool loadLevels(const std::string& levelListFile);
void loadProgress();
void saveLevels(const std::string& levelListFile);
void saveLevelProgress();
void nextLevel();
};
#endif

File Metadata

Mime Type
text/x-diff
Expires
Sat, May 16, 6:05 AM (13 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
62950
Default Alt Text
(347 KB)

Event Timeline