/** BOINGERS 2
  A game for the retro game competition on spheredev.org.
  http://www.spheredev.org/smforums/index.php/topic,4028.0.html
  
  Version 2.0
  Made by DaVince & Chris from Tengudev.
  http://tengudev.com/
**/


var VERSION = "alpha 2012-04-09";
var FPS = 50;
var SW = GetScreenWidth();
var SH = GetScreenHeight();
var GAME = undefined;
var ENEMIES = [];  //Array of any moving object on the map that needs to update.
var ENEMIES_NAMES;

var PLAYER = undefined;
var CAMERA = undefined;
var LEVEL = undefined;
var SCOREBOARD = undefined;
var MAINMENU = undefined;
var SETTINGS = undefined;
var MUSIC = undefined;

RequireScript("menu.js");
RequireScript("bouncytext.js");
RequireScript("resource.js");
RequireScript("level.js");
RequireScript("player.js");
var res = new ResourceManager();

RequireScript("input.js");
var KEYBOARD = new Controls();
var JOYSTICK0 = new Controls("joystick", 0);
var JOYSTICK1 = new Controls("joystick", 1);
function MultiInput() {
  this.pressed = function(btn) {
    return (KEYBOARD.pressed(btn) || JOYSTICK0.pressed(btn) || JOYSTICK1.pressed(btn));
  }
}
var INPUT = new MultiInput();


/** GAME START **/
function game()
{
  //Preload sound effects and set their volume.
  res.load("coin01.ogg").setVolume(128);
  res.load("coin02.ogg").setVolume(128);
  res.load("blip02.ogg").setVolume(128);
  res.load("explode02.ogg");
  res.load("fall01.ogg");
  res.load("explode04.ogg");
  res.load("explode06.ogg");
  res.load("joystix.rfn");

  MUSIC = new MusicPlayer();
  SCOREBOARD = new ScoreBoard();
  MAINMENU = new MainMenu();
  GAME = new Game();
  SETTINGS = new GameSettings();
	SETTINGS.load();
  PLAYER = new Player("player");

  MAINMENU.mainMenuScreen();
}




/** Game logic that happens before a level is open. **/
function Game()
{
	this.sound = true;
	this.music = true;
	
  this.init = function()
  {
    this.currentlevel = 0;
    this.ended = false;  //Set to true when game ends.
    this.levels = [];  //Level list
    this.levelset = "";
  }
  this.init();


  /** Select a levelset to play. **/
  this.selectLevelSet = function(purpose)
  {
    PLAYER.init();  //Do this only ONCE per game or you lose all stats!
    this.init();

    var levelsetmanager = new LevelSetManager();
    levelsetmanager.openMenu();
    if (this.levels.length == 0) {  return; };
    var levelset = GAME.levelset;
    
    if (purpose == "play" || purpose == undefined)
			this.playLevelSet();
		else if (purpose == "scoreboard")
			SCOREBOARD.highScoreScreen(GAME.levelset, false);

    //Either all levels are finished at this point, or the player died.
    SCOREBOARD.highScoreScreen(levelset, true);
  }
  
  
  /** Start the default levelset as a new game. **/
  this.startNewGame = function()
  {
		PLAYER.init();
		var levelsetmanager = new LevelSetManager();
		levelsetmanager.openLevelSet("default");
		
		this.playLevelSet();
		SCOREBOARD.highScoreScreen("default", true);
  }
  
  
  /** Play all the levels that are queued in the GAME.levels property. **/
  this.playLevelSet = function()
  {
		res.load("music/theme.ogg").stop();
		for (var i = 0; i < this.levels.length; i++)
    {
      LEVEL = new Level(this.levels[this.currentlevel][0], res.load(this.levels[this.currentlevel][1]));
      LEVEL.start();
      this.currentlevel++;
      if (this.ended) break;
    }
    
    this.end();
  }


  /** This happens on every map start, no matter the circumstances. **/
  this.mapStart = function()
  {
    PLAYER.resetMovement();

    //feed commands to certain persons in the map.
    var list = GetPersonList();
    for (var i = 0; i < list.length; i++)
    {
      if (list[i].substring(0, 7) == "cupcake")
        SetPersonScript(list[i], SCRIPT_COMMAND_GENERATOR, "QueuePersonCommand('"+list[i]+"', COMMAND_ANIMATE, true);");
        
      else if (list[i].substring(0, 8) == "warp-pad")  //Fix positions of warp pads
      {
        SetPersonX(list[i], GetPersonX(list[i])+8);
				SetPersonY(list[i], GetPersonY(list[i])+7);
      }

      else if (list[i].substring(0, 5) == "hplus" ||
               list[i].substring(0, 7) == "bubble_" ||
               list[i].substring(0, 7) == "feather" ||
               list[i].substring(0, 6) == "hfloat")
      {
        SetPersonValue(list[i], "type", "powerup");
        SetPersonScript(list[i], SCRIPT_COMMAND_GENERATOR, "QueuePersonCommand('"+list[i]+"', COMMAND_ANIMATE, true);");
 				for (var j = 0; j < PLAYER.powerups.got.length; j++)
				{
					if (PLAYER.powerups.got[j] == LEVEL.mapfile)
            DestroyPerson(list[i]);
				}
      }
      
      else if (list[i].substring(0, 9) == "smallfire" ||
               list[i].substring(0, 10) == "smallspike")
      {
        SetPersonScript(list[i], SCRIPT_COMMAND_GENERATOR, "QueuePersonCommand('"+list[i]+"', COMMAND_ANIMATE, true);");
        SetPersonValue(list[i], "type", "enemy");
      }
    }

    ENEMIES_NAMES = this.getEnemiesNames();
  }


  this.getEnemiesNames = function()
  {
    var array = [];
    for (var i = 0; i < ENEMIES.length; i++)
      array.push(ENEMIES[i].name);
    return array;
  }
  
  
  /** Game has ended and user is returned to main menu. Reinit some stuff. **/
  this.end = function()
  {
		LEVEL.end();
		GAME.ended = true;
		GAME.init();
		MAINMENU.init(true);
  }
}




/** The levelset menu.
  * A levelset is a text file containing the (order of) maps to open. **/
function LevelSetManager()
{
  /** Search for levelsets from the maps directory. **/
  this.getLevelSets = function()
  {
    var filelist = GetFileList("maps");
    var list = [];
    for (var i = 0; i < filelist.length; i++)
    {
      if (filelist[i].substr(filelist[i].length-8) == ".set.txt")
        list.push(filelist[i].substr(0, filelist[i].length-8));
    }
    return list;
  }


  /** Open a levelset file and fill in the game's levels array with them. **/
  this.openLevelSet = function(levelset)
  {
		GAME.levelset = levelset;
    levelset += ".set.txt";
    var file = OpenRawFile("../maps/" + levelset, false)
    var levels = [];

    var rawdata = file.read(file.getSize());
    rawdata = CreateStringFromByteArray(rawdata);
    var entries = rawdata.split("\n");

    for (var i = 0; i < entries.length; i++)
    {
      if (entries[i].substring(0, 1) != "#")  //If not a comment
      {
        var entry = entries[i].split("  ");
        entry[1] = "music/" + entry[1];
        if (entry.length == 2 && entry[0].substr(entry[0].length-3) == "rmp")
          levels.push(entry);
      }
    }

    GAME.levels = levels;
  }


  /** Open a levelset menu, unless there is only one levelset. **/
  this.openMenu = function()
  {
    var list = this.getLevelSets();
    if (list.length == 1)
    {
      this.openLevelSet(list[0]);
      return;
    }

    //Open a menu in all other cases.
    var selected;
    var menu = new GridMenu(10, 24, 3, SW-20, SH-20);
    if (GAME.sound) menu.sound = res.items["coin02.ogg"];
    if (GAME.sound) menu.confirmsound = res.items["coin01.ogg"];
    menu.arrow = res.load("arrow.png");
    menu.arrow_pos = [-6, 4];
    var instruct = new ScrollingText("=== select a levelset ===");
    menu.bgRender = function()
    {
      MAINMENU.renderMainMenu();
      instruct.update();
      instruct.render();
    }

    var opn;
    for (var i = 0; i < list.length; i++)
    {
      opn = new MenuOption(list[i]);
      menu.addOption(i%3, Math.floor(i/3), opn);

    }
    selected = menu.open();
    if (selected == undefined)  //This means the player canceled.
    {
      while (INPUT.pressed("cancel"));  //To avoid immediate quit in main menu.
      return;
    }

    this.openLevelSet(selected.text);
  }
}




/** The game's main menu. **/
function MainMenu()
{
  this.font = res.load("joystix.rfn");

  /** (Re)initialize some crucial parts of the menu. **/
  this.init = function(reinit)
  {
    SetFrameRate(FPS);
    if (!res.load("music/theme.ogg").isPlaying())
      MUSIC.play("music/theme.ogg", true);
    this.reinited = reinit;
  }

  /** Show the main menu. **/
  this.menuy = 30;
  this.menux = 130;
  this.mainMenuScreen = function()
  {
    this.init();
    this.timer = 0;

    //PRESS START part.
    var pressedlast = true;
    var starttext = new BouncyText(this.font, "PRESS START");
    while (!INPUT.pressed("action"))
    {
      this.renderMainMenu();
      starttext.update();
      starttext.draw(180, this.menuy+40);
      FlipScreen();
    }

    //Pressed "action".
    if (GAME.sound) res.load("coin01.ogg").play(false);
    while (AreKeysLeft())
    {
      this.renderMainMenu();
      FlipScreen();
      GetKey();
    }

    //Show menu options.
    var sel = 0;
    pressedlast = true;
    var options = [new BouncyText(this.font, "Play game"),
                   new BouncyText(this.font, "Play custom levelset"),
                   new BouncyText(this.font, "High scores"),
                   new BouncyText(this.font, "Settings"),
                   new BouncyText(this.font, "Exit game")];
    while (true)
    {
      if (this.reinited != undefined) break;

      //Rendering.
      this.renderMainMenu();
      this.font.setColorMask(COLORS.YELLOW);
      this.font.drawText(this.menux-10, this.menuy+2+(sel*20), "*");
      this.font.setColorMask(COLORS.WHITE);

      options[sel].update();
      for (var i = 0; i < options.length; i++)
        options[i].draw(this.menux, this.menuy+i*20);

      FlipScreen();

      //Controls.
      if (!pressedlast)
      {
        if (INPUT.pressed("up") || INPUT.pressed("down"))
        {
          res.load("coin02.ogg").stop();
          if (GAME.sound) res.load("coin02.ogg").play(false);
          for (var i = 0; i < options.length; i++)
            options[i].init();
        }

        if (INPUT.pressed("up") && sel > 0)
          sel--;
        else if (INPUT.pressed("up") && sel == 0)
          sel = options.length-1;

        else if (INPUT.pressed("down") && sel < options.length-1)
          sel++;
        else if (INPUT.pressed("down") && sel == options.length-1)
          sel = 0;

        else if (INPUT.pressed("confirm"))
        {
          res.load("coin01.ogg").stop();
          if (GAME.sound) res.load("coin01.ogg").play(false);

          //Main menu actions.
          switch (options[sel].text)
          {
						case "Play game":
							GAME.startNewGame();
							break;
          
						case "Play custom levelset":
							GAME.selectLevelSet();
							break;
          
						case "High scores":
							GAME.selectLevelSet("scoreboard");
							break;
						
						case "Settings":
							SETTINGS.openMenu();
							break;

						default:
							Exit();
          }
        }
      }

      if (INPUT.pressed("any")) pressedlast = true;
      else pressedlast = false;

      if (IsKeyPressed(KEY_ESCAPE))
        Exit();
    }
    if (this.reinited != undefined)
    {
      this.reinited = false;
      this.mainMenuScreen();
    }
    SetFrameRate(0);
  }

  /** Updates animations in the main menu. **/
  var playerpics = res.load("bigplayer.rss").images;
  //var enemypics = res.load("bigenemy.rss").images;
  var jumpy = 0;
  var imgcount = 0; //nth image to show
  var images = GetFileList("images/menubg/");
  var imgamount = images.length;
  var img = res.load("menubg/"+images[imgcount]);
  var imgtime = 0; //Amount of time passed for this image
  var imgmaxtime = 48*8;
  var mask = CreateColor(0,0,0);
  this.renderMainMenu = function()
  {
    this.renderBackgrounds();
    
		//Jumpy animations!
		if (GAME.music) this.timer++;
		else this.timer = 0;
		if (this.timer%48 == 1 || this.timer%48+1 == 1) jumpy = 2;
		else jumpy = 0;
		
		res.load("logo.png").zoomBlit(2, 125-jumpy, 0.98+jumpy/80);
    playerpics[3].blit(270, 180-jumpy);
    //enemypics[1].blit(270, 180-jumpy);
    
    Rectangle(0, 0, SW, 10, COLORS.TRANSPARENT_BLACK);
    res.load("tinyfont.rfn").setColorMask(COLORS.GREY);
    res.load("tinyfont.rfn").drawText(224, 0, "F10 - fullscreen");
    res.load("tinyfont.rfn").drawText(0, 0, VERSION);
  }
  
  this.renderBackgrounds = function()
  {
    //Show, scroll and fade the background pics.
    imgtime++;
    if (imgtime < 48) mask.alpha = (imgtime/48)*-255;
    if (imgtime > imgmaxtime-48) mask.alpha = ((imgmaxtime-imgtime)/48)*-255;
    if (imgtime >= imgmaxtime)
    {
      mask.alpha = 255;
      imgtime = 0;
      imgcount++;
      if (imgcount >= imgamount) imgcount = 0;
      img = res.load("menubg/"+images[imgcount]);
    }
    img.blit(-imgtime*((img.width-SW)/imgmaxtime),0);
    Rectangle(0, 0, SW, SH, mask);
  }
}





/** Manage game settings. **/
function GameSettings()
{
  /** Game settings menu. **/
  this.openMenu = function()
  {
		//Prepare the menu
    var selected = 0;
		var menu = new GridMenu(130, 20, 1, SW-20, SH-20);
		menu.arrow = res.load("arrow.png");
		menu.arrow_pos = [-6, 6];
	
		var instruct = new ScrollingText("=== Settings ===");
		menu.bgRender = function()
		{
			instruct.update();
			instruct.render();
			MAINMENU.renderMainMenu();
		}
		
		//Open and loop the menu
		while (true)  //Keep doin' it 'till we're broken out of here with a return
    {
			//Turn on/off sound effects in this menu on the spot
			menu.sound = GAME.sound ? res.items["coin02.ogg"] : undefined;
			menu.confirmsound = GAME.sound ? res.items["coin01.ogg"] : undefined;
			
			menu.addOption(0, 0, new MenuOption("Music: " + (GAME.music ? "on" : "off")));
			menu.addOption(0, 1, new MenuOption("Sound effects: " + (GAME.sound ? "on" : "off")));
			//menu.addOption(0, 2, new MenuOption("Configure keyboard"));
			//menu.addOption(0, 3, new MenuOption("Configure gamepad"));
			menu.addOption(0, 2, new MenuOption("Credits"));
			menu.addOption(0, 3, new MenuOption("Back"));
			
			//Set some stuff for the menu options.
			for (i in menu.options[0]) {
				menu.options[0][i].font = res.load("joystix.rfn");
				menu.options[0][i].selected_font = res.load("joystix.rfn");
			}
			
			selected = menu.open();
			if (selected == undefined)  //This means the player canceled.
			{
				while (INPUT.pressed("any"));  //To avoid immediate quit in main menu.
				return;
			}
			
			//Check what option was picked.
			switch (selected.y)
			{
				case 0: //Music
					GAME.music = !GAME.music;
					if (!GAME.music) MUSIC.stop();
					else MUSIC.play("music/theme.ogg", true);
					break;
				 
				case 1: //Sound
					GAME.sound = !GAME.sound;
					break;
				/*
				case 2: //Config keys
					Abort("TODO: implement key configuring.\n\nFor now, keys are:\nSpace - jump and confirm\nEscape - cancel and menu\nUp - float\nF7: MAGIC!\nF10: Fullscreen\nF11: show FPS");
					break;
				
				case 3: //Config gamepad
					Abort("TODO: implement gamepad configuring.\n\nFor now, buttons are:\nBtn 1 - Jump\nBtn 2 - Float\nBtn 3 - Menu, cancel\nI definitely want to be able to bind my START button to START...");
					break;
			  */
			  case 2: //Credits
			    CreditsScreen();
			    break;
				
				default:
					return;
					break;
			}
			this.save();
    }
    
  }
  
  /** Load stored settings (or use default settings) **/
  this.load = function()
  {
    var file = OpenFile("../settings.cfg", false);
    GAME.sound = file.read("sound", true);
    GAME.music = file.read("music", true);
    //TODO: key stuff
  }
  
  /** Save settings. **/
  this.save = function()
  {
    var file = OpenFile("../settings.cfg", true);
    file.write("sound", GAME.sound);
    file.write("music", GAME.music);
    //TODO: key stuff.
  }
}





/** The high score board, including functions to load and save high scores from and to file. **/
function ScoreBoard()
{
  this.font = res.load("joystix.rfn");
  this.scores = [];
  this.names = [];
  this.times = [];
  this.levelset = "";

  /** The high score screen.
    * @param allow_name_entry: boolean; determines whether name entry should be allowed. **/
  this.highScoreScreen = function(levelset, allow_name_entry)
  {
    SetFrameRate(FPS);
    this.levelset = levelset;
    if (LEVEL) LEVEL.music.stop();
    res.load("music/theme.ogg").stop();
    MUSIC.play("music/highscore.ogg", true);

    this.load(levelset);
    var scrolltext = new ScrollingText("High Scores for " + levelset);

    if (allow_name_entry)
      this.nameEntryScreen();

    var colors = [];
    for (var i = 0; i < 10; i++)
      colors[i] = CreateColor(40+Math.cos(i)*30, 255-i*20, 100+i*20);

    while(!INPUT.pressed("menu"))
    {
      //Show scores here.
      this.font.setColorMask(COLORS.GREY);
      scrolltext.update();
      scrolltext.render();

      for (var i = 0; i < 10; i++)
      {
        this.font.setColorMask(colors[i]);
        this.font.drawText(10, 32+i*18, this.names[i]);
        this.font.drawText(210, 32+i*18, this.scores[i]);
        this.font.drawText(260, 32+i*18, this.times[i]);
      }

      this.font.setColorMask(COLORS.GREY);
      res.load("tinyfont.rfn").drawText(60, 225, "Escape (or button 3) - to main menu");
      FlipScreen();
    }

    SetFrameRate(0);
    res.load("music/highscore.ogg").stop();
    MAINMENU.mainMenuScreen();
  }


  /** Name entry screen (for if you have a high score).
    * Aborts immediately when there is no new hi score. **/
  this.nameEntryScreen = function()
  {
    var hiscorepos = this.checkHighScore();
    if (hiscorepos == -1) return;

    var bouncytext = new BouncyText(this.font, "YOU GOT A NEW HIGH SCORE!");
    var playername = "";
    var nametextpos = 0;
    var lastpressed = true;
    var key = 0;
    var shift = false;
    while (AreKeysLeft()) GetKey();

    //Check high scores here and enter name if true.
    while(true)
    {
      this.font.setColorMask(COLORS.GREEN);
      bouncytext.update();
      bouncytext.draw(45, 40);
      this.font.setColorMask(COLORS.YELLOW);
      this.font.drawText(56, 70, "Please enter your name. ");

      this.font.setColorMask(COLORS.WHITE);
      nametextpos = (SW-this.font.getStringWidth(playername+"_"))/2;
      this.font.drawText(nametextpos, 100, playername + "_");
      FlipScreen();

      //Input.
      while (AreKeysLeft()) key = GetKey();

      if (key != undefined)
      {
        if (key == KEY_BACKSPACE)
          playername = playername.slice(0, playername.length-1);
        else if (key == KEY_ENTER && !lastpressed && playername != "")
          break;
        else if (playername.length <= 20)
          playername += GetKeyString(key, shift);
      }

      key = 0;

      lastpressed = false;
      if (IsAnyKeyPressed()) lastpressed = true;
      shift = false;
      if (IsKeyPressed(KEY_SHIFT)) shift = true;
    }

    //Now add the score to the table.
    this.names.splice(hiscorepos, 0, playername);
    this.scores.splice(hiscorepos, 0, PLAYER.score);
    this.names = this.names.slice(0, 10);  //No more than 10 entries
    this.scores = this.scores.slice(0, 10);
    this.save(this.levelset);
  }


  /** Check if the player has a high score. **/
  this.checkHighScore = function()
  {
    for (var i = 0; i < this.scores.length; i++)
    {
      if (PLAYER.score >= this.scores[i])
        return i;
    }
    return -1;  //No hiscore.
  }


  /** Load scores from file. **/
  this.load = function(levelset)
  {
		//Check if score file exists first.
		var file;
		var files = GetFileList("other");
		var fileexists = false;
		for (i in files)
		{
			if (files[i] == levelset+"-scores.dat") fileexists = true;
		}
		
		if (fileexists) { file = OpenRawFile(levelset+"-scores.dat", false); }
		else { file = OpenRawFile("scores.dat", false); } //Load default scoreboard if scores didn't exist.
		
		
    var rawdata = file.read(file.getSize());
    rawdata = CreateStringFromByteArray(rawdata);
    var entries = rawdata.split("\n");

    //Fill in the entries.
    for (var i = 0; i < 10; i++)
    {
      this.names[i] = entries[i*3];
      this.scores[i] = entries[i*3+1];
      this.times[i] = entries[i*3+2];
    }
    file.close();
  }


  /** Save scores to file. **/
  this.save = function(levelset)
  {
    var file = OpenRawFile(levelset+"-scores.dat", true);

    for (var i = 0; i < this.names.length; i++)
    {
      file.write(CreateByteArrayFromString(this.names[i]+"\n"));
      file.write(CreateByteArrayFromString(this.scores[i]+"\n"));
      file.write(CreateByteArrayFromString(this.times[i]+"\n"));
    }

    file.close();
  }
}





/** Scrolling text box shown at the top. **/
function ScrollingText(text)
{
  this.text = text;
  this.font = res.load("joystix.rfn");
  this.textwidth = this.font.getStringWidth(text);
  this.x = SW;

  this.update = function()
  {
    if (!GAME.ended)
    {
      if (INPUT.pressed("down")) this.xspeed = 5;
      else this.xspeed = 3;
    }
    this.x -= this.xspeed;
    if (this.x < -this.textwidth) this.x = SW;
  }

  this.render = function()
  {
    Rectangle(0, 0, SW, 14, COLORS.BLACK);
    this.font.drawText(this.x, 0, this.text);
  }
}





/** Text box shown inside the level. **/
function TextBox(text)
{
  this.text = text;
  this.font = res.load("joystix.rfn");
  this.textwidth = this.font.getStringWidth(text);
  var x = 32;
  var y = 40;
  var w = SW-x*2;
  var h = 70;
  
  this.render = function()
  {
    Rectangle(x, y, w, h, COLORS.TRANSPARENT_BLACK);
    this.font.drawTextBox(x, y, w, h, 0, this.text);
  }
}





/** Projectile firing. **/
var PROJECTILECOUNT = 0; //Global projectile count to make sure projectiles don't affect one another somehow.
function Projectile(owner, direction)
{
  this.owner = owner;
  this.dir = direction;
  this.name = "projectile"+PROJECTILECOUNT;
  this.time = 0;
  this.maxtime = 20;
  
  this.init = function()
  {
    PROJECTILECOUNT++;
    CreatePerson(this.name, "dagger.rss", true);
    if (owner == PLAYER.name) PLAYER.projectiles.push(this);
    LEVEL.projectiles.push(this);
    SetPersonX(this.name, GetPersonX(owner));
    SetPersonY(this.name, GetPersonY(owner));
    if (this.dir < 0) SetPersonDirection(this.name, "west")
  }
  
  
  this.update = function()
  {
    SetPersonX(this.name, GetPersonX(this.name)+this.dir*6);
    this.time++;
    if (GetPersonX(this.name) > GetPersonX(this.owner)+PLAYER.projectiledistance ||
        GetPersonX(this.name) < GetPersonX(this.owner)-PLAYER.projectiledistance)
      this.destroy();
  }
  
  
  this.destroy = function()
  {
    if (this.owner == PLAYER.name)
    {
      var idx = IndexOf(PLAYER.projectiles, this); // Find the index
      if(idx!=-1) PLAYER.projectiles.splice(idx, 1);
    }
    idx = IndexOf(LEVEL.projectiles, this); // Find the index
    if(idx!=-1) LEVEL.projectiles.splice(idx, 1);
    DestroyPerson(this.name);
  }
  this.init();
}





/** Play and switch music. **/
function MusicPlayer()
{
  this.oldsong = undefined;
  this.song = undefined;
  
  /** Play a song file. Can take a string or sound object as the argument. **/
  this.play = function(song, loop)
  {
    //if (this.oldsong == this.song) return;
    this.oldsong = this.song;
    if (!GAME.music) return;
    
    if (typeof(song) == "string") //File string
		{
			if (res.items[song] == undefined) res.load(song);
			this.song = res.items[song];
		}
		else //Sound object
		{
		  this.song = song;
		}
		
		if (this.oldsong != undefined) this.oldsong.stop();
		this.song.play(loop);
  }
  
  this.stop = function()
  {
    if (this.song) this.song.stop();
  }
}




/** The game credits. **/
function CreditsScreen()
{
  //MUSIC.play("music/credits.ogg", true);
  var hugefont = res.load("joystix.rfn");
  var bigfont = res.load("joystix.rfn");
  var smallfont = res.load("tinyfont.rfn");
  var ypos = SH;
  
  var credits = [32,
    res.load("logo.png"),
    [bigfont, COLORS.WHITE, "And the Quest for Cupcakes"],
    [smallfont, COLORS.GREY, "(and Justice)"],
    64,
    [smallfont, COLORS.GREY, "A game by"],
    [bigfont, COLORS.WHITE, "Tengudev"],
    96,
    [smallfont, COLORS.WHITE, "~ Originally made for a competition in 2010 ~"],
    32,
    [smallfont, COLORS.GREY, "Enhanced version made in 2012"],
    64,
    [smallfont, COLORS.GREY, "Game design"],
		[bigfont, COLORS.WHITE, "Vincent Beers"],
		[bigfont, COLORS.WHITE, "Chris Crawford"],
		64,
		[smallfont, COLORS.GREY, "Tile graphics"],
		[bigfont, COLORS.WHITE, "Vincent Beers"],
		[bigfont, COLORS.WHITE, "Chris Crawford"],
		64,
		[smallfont, COLORS.GREY, "Sprite graphics"],
		[bigfont, COLORS.WHITE, "Chris Crawford"],
		64,
		[smallfont, COLORS.GREY, "Coding"],
		[bigfont, COLORS.WHITE, "Vincent Beers"],
		64,
		[smallfont, COLORS.GREY, "Music"],
		[bigfont, COLORS.WHITE, "Vincent Beers"],
		64,
		[smallfont, COLORS.GREY, "Maps"],
		[bigfont, COLORS.WHITE, "Vincent Beers"],
		[smallfont, COLORS.WHITE, "Old maps by Chris Crawford"],
		64,
		[bigfont, COLORS.GREY, "Joystix font"],
		2,
		[bigfont, COLORS.WHITE, "Larabie Fonts"],
		[bigfont, COLORS.YELLOW, "http://www.larabiefonts.com/"],
		64,
		[smallfont, COLORS.GREY, "tinyfont"],
		2,
		[smallfont, COLORS.WHITE, "Based on Andale Mono"],
		[smallfont, COLORS.GREY, "(Part of MS core fonts)"],
		64,
		[smallfont, COLORS.GREY, "Made using the"],
		res.load("spherelogo.png"),
		[bigfont, COLORS.YELLOW, "http://spheredev.org"],
		64,
		[smallfont, COLORS.GREY, "Web site"],
		[bigfont, COLORS.YELLOW, "boingers.tengudev.com"],
		128,
		[bigfont, COLORS.LIGHTBLUE, "Thank you for playing!"] //Maybe put this in a separate fade-in at the end.
    ];
  
  //Calculate x and y positions of the credit elements.
  var x = [];
  var y = [];
  var height = 0; //Credits total height.
  for (var i = 0; i < credits.length; i++)
  {
    y[i] = height;
    
    //Is a picture:
    if (credits[i].width)
    {
      x[i] = credits[i].width;
      height+=credits[i].height+5;
    }
    //Is a number (to add empty areas):
    else if (typeof credits[i] == "number")
    {
      x[i] = 0;
      height += credits[i];
    }
    //Is a text string:
    else if (credits[i].length == 3)
    {
      x[i] = credits[i][0].getStringWidth(credits[i][2]);
      height += credits[i][0].getHeight()+2;
    }
    
    //Set the actual x position.
    x[i] = (SW-x[i])/2;
	}
  
  //Display that stuff!
  while(ypos > -height)
  {
    //res.items["tinyfont.rfn"].drawText(0, 0, ypos); //Debug
    MAINMENU.renderBackgrounds();
		for (var i = 0; i < credits.length; i++)
		{
			if (credits[i].length == 3) //Is text
			{
			  Rectangle(x[i]-2, ypos+y[i]-1, credits[i][0].getStringWidth(credits[i][2])+4, credits[i][0].getHeight()+2, COLORS.TRANSPARENT_BLACK);
				font = credits[i][0];
				font.setColorMask(credits[i][1]);
				font.drawText(x[i], ypos+y[i], credits[i][2]);
			}
			else if (credits[i].width) //Is a pic
				credits[i].blit(x[i], ypos+y[i]);
		}
	  
		FlipScreen();
		ypos-=0.8;
		if (INPUT.pressed("action")) ypos-=4;
	}
}



/** Function does not exist in Sphere 1.5, so adding. **/
IndexOf = function(arr, text)
{
	var i = arr.length;
	while(--i > -1) if(arr[i] == text) return i;
	return i;
}
