/** BOINGERS 2
  A game for the retro game competition on spheredev.org.
  http://www.spheredev.org/smforums/index.php/topic,4028.0.html

  Made by DaVince from Tengudev.
  http://tengudev.com/
**/


const FPS = 30;
const SW = GetScreenWidth();
const 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;

RequireScript("menu.js");
RequireScript("bouncytext.js");
RequireScript("resource.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");

  SCOREBOARD = new ScoreBoard();
  MAINMENU = new MainMenu();
  GAME = new Game();
  PLAYER = new Player("player");

  MAINMENU.mainMenuScreen();
}




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


  /** Start a new game. **/
  this.startNewGame = function(tutorial_mode)
  {
    PLAYER.init();  //Do this only ONCE per game or you lose all stats!
    this.init();

    if (tutorial_mode)
    {
      res.items["music/rin-theme.ogg"].stop();
      PLAYER.lives = Infinity;
      LEVEL = new Level("default/tutorial.rmp");
      LEVEL.start();
      GAME = new Game();
      MAINMENU.init(true); return;
    }

    var levelset = new LevelSetManager();
    levelset.openMenu();
    if (this.levels.length == 0) { MAINMENU.init(true); return; };
    res.items["music/rin-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;
    }

    //Either all levels are finished at this point, or the player died.
    SCOREBOARD.highScoreScreen(true);
  }


  /** 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);");
        
        for (var j = 0; j < LEVEL.gotcupcakes.length; j++)
          if (LEVEL.gotcupcakes[j] == list[i])
            DestroyPerson(list[i]);
      }

      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");
        
 				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;
  }
}




/** 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)
  {
    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("  ");
        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, 16, 2, SW-20, SH-20);
    menu.font = GAME.font;
    menu.sound = res.items["coin02.ogg"];
    menu.confirmsound = res.items["coin01.ogg"];
    var instruct = new TextBox("=== select a levelset to play ===");
    menu.bgRender = function()
    {
      instruct.update();
      instruct.render();
    }

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

    selected = menu.open();
    if (selected == undefined)  //This means the player canceled.
    {
      while (IsKeyPressed(KEY_ESCAPE));  //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");
  res.load("music/rin-theme.ogg")

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

  /** Show the main menu. **/
  this.menuy = 100;
  this.mainMenuScreen = function()
  {
    this.init();

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

    //Pressed "action".
    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, "Start game"),
                   new BouncyText(this.font, "Tutorial"),
                   new BouncyText(this.font, "High scores"),
                   new BouncyText(this.font, "Exit game")];
    while (true)
    {
      if (this.reinited != undefined) break;

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

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

      FlipScreen();

      //Controls.
      if (!pressedlast)
      {
        if (INPUT.pressed("up") || INPUT.pressed("down"))
        {
          res.load("coin02.ogg").stop();
          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 = 3;

        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();
          res.load("coin01.ogg").play(false);

          //Main menu actions.
          if (options[sel].text == "Start game")
            GAME.startNewGame();

          else if (options[sel].text == "High scores")
            SCOREBOARD.highScoreScreen(false);

          else if (options[sel].text == "Tutorial")
            GAME.startNewGame(true);

          else
            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;
  this.renderMainMenu = function()
  {
    res.load("titlescreen.png").blit(0, 0);

    playerpics[3].blit(170, 157);
    enemypics[1].blit(220, 150);
  }
}




/** 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 = [];

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

    this.load();
    var scrolltext = new TextBox("High Scores");

    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(20, 32+i*18, this.names[i]);
        this.font.drawText(220, 32+i*18, this.scores[i]);
      }

      this.font.setColorMask(COLORS.GREY);
      res.load("tinyfont.rfn").drawText(15, 225, "Press btn 3 (esc) to go back to the main menu.");
      FlipScreen();
    }

    SetFrameRate(0);
    res.items["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;
    var shift = false;

    //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 = "";

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

    //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();
  }


  /** 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()
  {
    var file = OpenRawFile("scores.dat", false);
    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*2];
      this.scores[i] = entries[i*2+1];
    }
    file.close();
  }


  /** Save scores to file. **/
  this.save = function()
  {
    var file = OpenRawFile("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.close();
  }
}



/** Level system.
  * Initializes the entire level, with enemies, items etc.
  * Also has the background code for any important game event that happen in-level. That is,
  * the win screen, lose screen and pause menu, and the triggers for all of this.
  **/
function Level(mapfile, music)
{
  //IDEA SCRAPPED/DELAYED: level generation
  this.font = res.load("joystix.rfn");
  this.timer = 0;
  this.paused = false;
  this.items = [];
  this.mapfile = mapfile;
  this.music = music ? music : res.load("music/levelsong1.ogg");
  this.powerupmusic = res.load("music/powerup.ogg");
  this.gotcupcakes = [];

  //Help text to display at the top of the screen.
  this.helptext = "";


  //Do I need this? Should I just start up the game on init, since I'll have to have a map open in order to modify it?
  this.start = function()
  {
    this.music.play(true);
    SetRenderScript("LEVEL.renderHUD();");
    ENEMIES = [];
    MapEngine(this.mapfile, FPS);
  }


  /** Constructor of this object, sort of. **/
  this.init = function()
  {
    PLAYER.resetMovement();
    LEVEL = this;  //Just to be sure this can be referenced everywhere.
    SetUpdateScript("LEVEL.update();");
    SetDefaultMapScript(SCRIPT_ON_ENTER_MAP, "CAMERA = new Camera('player'); GAME.mapStart();");

    /** Debugging/cheat keys! */
    //BindKey(KEY_D, "PLAYER.die();", "");
    BindKey(KEY_F7, "PLAYER.score = 0; LEVEL.win();", "");
    //BindKey(KEY_S, "PLAYER.score += 1000;", "");
    BindKey(KEY_ESCAPE, "", "");
    BindKey(KEY_R, "ChangeMap('" + this.mapfile + "');", "");  //Restart level
  }


  /** Update all game logic (as an UpdateScript). **/
  this.update = function()
  {
    this.timer++;

    //Control buttons
    if (INPUT.pressed("menu"))
      this.pauseMenu();

    if (this.powerupmusic.isPlaying()) this.music.stop();
    else if (!this.music.isPlaying()) this.music.play(true);

    //Update the rest now.
    PLAYER.update();
    CAMERA.update();

    for (var i = 0; i < ENEMIES.length; i++)
      ENEMIES[i].update();
  }


  res.load("hearts.rss");
  var time = 0;
  var textbox = new TextBox(this.helptext);
  var hts;  //For hearts renderer.
  /** Render the HUD (things like player score and health). **/
  this.renderHUD = function()
  {
    time = Math.floor(this.timer*10/FPS)/10;

    //Ugh, lol.
    this.font.setColorMask(COLORS.BLACK);
    this.font.drawText(7, 10, "SCORE: " + PLAYER.score);
    this.font.drawText(7, 20, "TIME: " + time);
    this.font.drawText(5, 10, "SCORE: " + PLAYER.score);
    this.font.drawText(5, 20, "TIME: " + time);
    this.font.drawText(6, 9, "SCORE: " + PLAYER.score);
    this.font.drawText(6, 19, "TIME: " + time);
    this.font.drawText(6, 11, "SCORE: " + PLAYER.score);
    this.font.drawText(6, 21, "TIME: " + time);

    this.font.setColorMask(COLORS.WHITE);
    this.font.drawText(6, 10, "SCORE: " + PLAYER.score);
    this.font.drawText(6, 20, "TIME: " + time);

    //Draw one full heart per 2 health and a health-dependent heart as the last.
    for (hts = 0; hts < (PLAYER.health-2)/2; hts++)
      res.items["hearts.rss"].images[2].blit(6+hts*14, 32);
    res.items["hearts.rss"].images[PLAYER.health-hts*2].blit(6+hts*14, 32);

    if (!this.paused)
      textbox.update();

    if (this.helptext != "")
      textbox.render();

    if (this.helptext != textbox.text)
      textbox = new TextBox(this.helptext);
  }


  /** The pause menu. Quickly hacked together to "just work". **/
  this.pauseMenu = function()
  {
    SetUpdateScript("");
    SetFrameRate(FPS);

    this.paused = true;
    var sel = 0;
    var pausetext = new BouncyText(this.font, "GAME PAUSED");
    var grav = 1;
    var gravdir = -1;

    while(!INPUT.pressed("cancel"))
    {
      UpdateMapEngine();
      RenderMap();
      Rectangle(0, 0, SW, SH, COLORS.TRANSPARENT_BLACK);

      grav += (gravdir*0.001);
      if (grav < 0.2) gravdir = 1;
      else if (grav > 1) gravdir = -1;
      pausetext.setGravity(grav);

      this.font.setColorMask(COLORS.GREY);
      pausetext.update();
      pausetext.draw(115+(Math.sin(grav*100)*5), 60);

      this.font.setColorMask(COLORS.YELLOW);
      this.font.drawText(120, 102+sel*15, "*");

      this.font.setColorMask(COLORS.WHITE);
      this.font.drawText(130, 100, "Continue");
      this.font.drawText(130, 115, "Quit");
      FlipScreen();

      //Input.
      if (INPUT.pressed("up") && sel)
      {
        res.load("coin02.ogg").stop();
        res.load("coin02.ogg").play(false);
        sel = 0;
      }
      else if (INPUT.pressed("down") && !sel)
      {
        res.load("coin02.ogg").stop();
        res.load("coin02.ogg").play(false);
        sel = 1;
      }
      else if (INPUT.pressed("confirm") && !sel)
      {
        res.load("coin01.ogg").play(false);
        break;
      }
      else if (INPUT.pressed("confirm") && sel)
      {
        GAME.ended = true;
        this.end();
        break;
      }
    }

    this.paused = false;
    SetFrameRate(0);
    SetUpdateScript("LEVEL.update();");
  }


  /** Death screen. **/
  this.dieScreen = function()
  {
    SetUpdateScript("");
    SetFrameRate(FPS);
    this.paused = true;

    if (PLAYER.lives < 0)
    {
      this.gameOverScreen();
      return;  //Avoid accidental continuation of this method.
    }

    var bouncytext = new BouncyText(this.font, "YOU DIED");
    var livesx = (SW-this.font.getStringWidth("Lives left: " + PLAYER.lives))/2;

    while(!INPUT.pressed("confirm"))
    {
      UpdateMapEngine();
      RenderMap();
      Rectangle(0, 0, SW, SH, COLORS.TRANSPARENT_BLACK);  //Make this transparent red maybe?

      this.font.setColorMask(COLORS.RED);
      bouncytext.update();
      bouncytext.draw(120, 60);
      this.font.setColorMask(COLORS.YELLOW);
      this.font.drawText(livesx, 80, "Lives left: " + PLAYER.lives);

      this.font.setColorMask(COLORS.WHITE);
      this.font.drawText(40, 120, "Press button 1 to continue.");

      FlipScreen();
    }

    SetFrameRate(0);
    SetUpdateScript("LEVEL.update();");

    this.paused = false;
    res.items["music/death.ogg"].stop();
    LEVEL.music.play(true);
    ChangeMap(this.mapfile);
  }


  /** Level won. **/
  this.win = function()
  {
    SetUpdateScript("");
    SetFrameRate(FPS);
    this.paused = true;

    this.music.stop();
    res.load("music/wintune.ogg").play(false);

    var endtime = Math.floor(this.timer*10/FPS)/10;
    var strings = [
      "LEVEL FINISHED",
      "Score: " + PLAYER.score,
      "Level time: " + endtime + " sec",
      "Lives left: " + PLAYER.lives,
      "Press button 1 to continue"];

    var stringsx = [];
    for (var i = 0; i < strings.length; i++)
      stringsx[i] = (SW - this.font.getStringWidth(strings[i])) / 2;

    var bouncytext = new BouncyText(this.font, strings[0]);
    var clr = CreateColor(0, 0, 0, 200);

    while (!INPUT.pressed("confirm"))
    {
      UpdateMapEngine();
      RenderMap();
      Rectangle(0, 0, SW, SH, clr);

      this.font.setColorMask(COLORS.GREEN);
      bouncytext.update();
      bouncytext.draw(stringsx[0], 60);
      this.font.setColorMask(COLORS.YELLOW);
      this.font.drawText(stringsx[1], 90, strings[1]);
      this.font.setColorMask(COLORS.WHITE);
      this.font.drawText(stringsx[2], 105, strings[2]);
      this.font.drawText(stringsx[3], 120, strings[3]);
      this.font.setColorMask(COLORS.GREY);
      this.font.drawText(stringsx[4], 150, strings[4]);

      FlipScreen();
    }

    res.items["music/wintune.ogg"].stop();
    this.end();
    SetFrameRate(0);
  }


  /** Game lost. **/
  this.gameOverScreen = function()
  {
    SetFrameRate(FPS);
    var scorewidth = this.font.getStringWidth("Final score: " + PLAYER.score);

    while (!INPUT.pressed("confirm"))
    {
      Rectangle(0, 0, SW, SH, COLORS.DARKRED);
      Rectangle(0, 0, SW, SH, COLORS.TRANSPARENT_BLACK);

      this.font.setColorMask(COLORS.BLACK);
      this.font.drawText(120, 60, "GAME OVER");
      this.font.setColorMask(COLORS.WHITE);
      this.font.drawText(121, 61, "GAME OVER");

      this.font.drawText((SW-scorewidth)/2, 80, "Final score: " + PLAYER.score);

      FlipScreen();
    }

    GAME.ended = true;
    this.end();
    SetFrameRate(0);
  }


  /** Level end, no matter the circumstances. **/
  this.end = function()
  {
    this.music.stop();
    ExitMapEngine();
  }

  this.init();  //Auto-invoked as a constructor of sorts
}




/** Camera system.
  * The camera moves on a screen-by-screen basis and the action pauses when
  * going from screen to screen, much like Mega man.
  * @param focus_on: the camera should be focusing on this Person.
**/
function Camera(focus_on)
{
  this.focus = focus_on ? focus_on : "player";
  this.x = Math.floor(GetPersonX(PLAYER.name)/SW);  //Every screen is considered 1 unit of x or y.
  this.y = Math.floor(GetPersonY(PLAYER.name)/SH);
  this.pixelx = this.x*SW;
  this.pixely = this.y*SH;

  this.update = function()
  {
    if (PLAYER.x < GetLayerWidth(0)*16 && PLAYER.y < GetLayerHeight(0)*16)
    {
      if (PLAYER.x < this.x*SW)  //Leave left
        this.scroll(this.pixelx-SW, this.pixely);
      if (PLAYER.x > this.x*SW+SW)  //Leave right
        this.scroll(this.pixelx+SW, this.pixely);
      if (PLAYER.y < this.y*SH)
        this.scroll(this.pixelx-SW, this.pixely);
      if (PLAYER.y > this.y*SH+SH)
        this.scroll(this.pixelx-SW, this.pixely);
    }
  }

  /** This could be done better, especially since I can disable the map events at will
      and just do it in-map, but what the heck. I just want to get this game finished. **/
  this.scroll = function(targetx, targety)
  {
    SetFrameRate(FPS);

    //TODO: reimplement.


    //Temp:
    this.x = Math.floor(GetPersonX(PLAYER.name)/SW);
    this.y = Math.floor(GetPersonY(PLAYER.name)/SH);
    this.pixelx = this.x*SW+SW/2;
    this.pixely = this.y*SH+SH/2;
    if (GetLayerHeight(0) == 16)  this.pixely += 16;
    //End temp

    SetCameraX(this.pixelx);
    SetCameraY(this.pixely);

    this.setObjectsActive();
    SetFrameRate(0);
  }

  /** Check the camera position and (dis)activate object that are currently (not) on the screen. **/
  this.setObjectsActive = function()
  {
    for (var i = 0; i < ENEMIES.length; i++)
    {
      if (Math.floor(ENEMIES[i].x/SW) == this.x)
        ENEMIES[i].active = true;
      else
        ENEMIES[i].active = false;
    }
  }
  this.setObjectsActive();
}




/** Path items are persons that move in a fixed path and don't have any sort of AI of their own.
  * HOW TO USE
  * 1. Add a new person to your map.
  * 2. Add something like this to its On Create code:
  *   new PathItem(GetCurrentPerson(), "platform", [["south", 100], ["north", 100]]);
  * 3. You're done.
  *
  * @param name: person name to associate with. Also takes starting x and y from here.
  * @param type: type of item; eg. enemy or platform.
  * @param path: array of movement instructions in the form of ["north", 100] or ["west", 20].
  **/
function PathItem(name, type, path)
{
  this.name = name;
  this.type = type ? type : "enemy";
  this.active = true;  //Camera system should make it active once on-screen.
  this.x = GetPersonX(this.name);
  this.y = GetPersonY(this.name);
  this.path = path;

  this.cur_path_item = 0;  //Current item in the path to check for instructions.
  this.cur_item_prog = 0;  //Progress in the current path item.

  this.init = function()
  {
    SetPersonValue(this.name, "new", true);
    SetPersonValue(this.name, "type", this.type);
    ENEMIES.push(this);
  }

  this.update = function()
  {
    if (GetPersonValue(this.name, "new")) SetPersonValue(this.name, "new", false);
    if (!this.active || this.path.length == 0) return;

    this.cur_item_prog++;
    if (this.cur_item_prog > this.path[this.cur_path_item][1])
    {
      this.cur_item_prog = 0;
      this.cur_path_item++;
    }

    if (this.cur_path_item >= this.path.length)
      this.cur_path_item = 0;

    switch(this.path[this.cur_path_item][0])
    {
      case "north":
        this.y--;
        break;
      case "south":
        this.y++;
        break;
      case "west":
        this.x--;
        break;
      case "east":
        this.x++;
        break;
    }
    SetPersonX(this.name, this.x);
    SetPersonY(this.name, this.y);
  }
  this.init();
}




/** Text box shown inside the level. **/
function TextBox(text)
{
  this.text = text;
  this.xspeed = 3;
  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, 16, COLORS.BLACK);
    this.font.drawText(this.x, 1, this.text);
  }
}




/** The player character, with stats and movement.
  * @param name: Player name. Useful if the game ever gets multiplayer capability.
**/
function Player(name)
{
  CreatePerson(name, name + ".rss", false);
  CreatePerson("bubble", "bubbleprotect.rss", false);

  this.init = function()
  {
    this.lives = 5;
    this.health = 2;
    this.maxhealth = 2;
    this.score = 0;
    this.cupcakes = 0;
    this.powerups = [];
    this.powerups.longer_float = 0;  //For each longer float powerup, you can float 10 frames longer.
    this.powerups.bubble = false;  //Defense bubble
    this.powerups.high_float = false;
    this.powerups.highfloattime = 0;
    this.powerups.maxhighfloattime = 90;  //About 3 high floats.
    this.powerups.got = [];  //Levels you got powerups from.

    //Navigational properties
    this.name = name;
    this.x = GetPersonX(name);
    this.y = GetPersonY(name);
    this.xspeed = 0;
    this.yspeed = 0;
    this.xdir = 1;
    this.gravity = 1;  //Gravity = jump speed.

    this.floating = false;
    this.floated_this_jump = false;
    this.floattime = 0;   //Amount of time already floated.
    this.jumped_prev_frame = false;  //Did the player hold the jump button in the previous frame?
    this.floorwait = 0;  //Wait for a set amount of time when on the floor.

    //Configurables
    this.fwmax = 6;     //Wait this amount of frames.
    this.max_floattime = 30;  //In frames
    this.max_friction = 15;  //For xspeed.
    this.maxfallspeed = 20;

    //Debugging
    //SetRenderScript("PLAYER.debug()");
  }
  this.init();


  var ignorelist;
  /** Update player. **/
  this.update = function()
  {
    SetPersonIgnoreList(this.name, GetPersonList());
    this.updateMovement();

    ignorelist = ["bubble"];
    if (this.alreadyhurt) ignorelist = ignorelist.concat(ENEMIES_NAMES);

    SetPersonIgnoreList(this.name, ignorelist);
    this.checkOverlap();
  }


  /** Done every time before a new map starts. **/
  this.resetMovement = function()
  {
    this.x = GetPersonX(name);
    this.y = GetPersonY(name);
    this.xspeed = 0;
    this.yspeed = 0;
    this.xdir = 1;
    this.gravity = 1;

    this.floating = false;
    this.floated_this_jump = false;
    this.floattime = 0;
    this.jumped_prev_frame = false;
    this.floorwait = 0;

    this.hurtcounter = 0;
    this.alreadyhurt = false;

    SetPersonVisible(this.name, true);
    this.health = this.maxhealth;

    SetPersonIgnoreList("bubble", GetPersonList());
    SetPersonX("bubble", -32);
    SetPersonY("bubble", -32);
  }


  /** Movement system for player. **/
  this.updateMovement = function()
  {
    //GRAVITY AND JUMPING
    //Also floating, and floating powerups.
    this.ydir = 1;
    if (this.yspeed > 0) this.ydir = 1;
    else if (this.yspeed < 0) this.ydir = -1;

    this.inair = (!IsPersonObstructed(this.name, this.x, this.y+1));
    if (this.inair)
    {
      if (this.jumped_prev_frame && INPUT.pressed("jump")) this.yspeed -= 0.5;
      else if (!INPUT.pressed("jump")) this.jumped_prev_frame = false;
      if (this.ydir == 1) this.jumped_prev_frame = false;
      if (this.yspeed < this.maxfallspeed) this.yspeed += this.gravity;

      //Floating mechanism//
      if (INPUT.pressed("fly") && !this.floated_this_jump
          && this.yspeed > 1 && this.yspeed < 11)
      {
        this.floating = true;
        if (this.powerups.high_float) this.yspeed = 0.5;
        else this.yspeed = 1;
        this.floattime++;
        if (this.powerups.high_float) this.powerups.highfloattime++;
      }

      //Float once, then finish.
      if (this.floating && !INPUT.pressed("fly"))
        this.floattime = this.max_floattime+(this.powerups.longer_float*10);

      if (this.powerups.high_float && this.powerups.highfloattime > this.powerups.maxhighfloattime)
      {
        this.powerups.highfloattime = 0;
        this.powerups.high_float = false;
      }

      //End floating.
      if (this.floattime >= this.max_floattime+(this.powerups.longer_float*10))
      {
        this.floating = false;
        this.floated_this_jump = true;
      }

      //Animation//
      if (this.floating)
        SetPersonFrame(this.name, 4+(this.floattime/this.max_floattime)*16%3);  //Frames 4 to 7 are fly
      else
        SetPersonFrame(this.name, 3);
    }

    else  //On the ground
    {
      this.floorwait++;
      this.floated_this_jump = false;
      this.floattime = 0;

      if (this.floorwait == this.fwmax)
      {
        this.floorwait = 0;
        this.jumped_prev_frame = INPUT.pressed("jump");
        if (this.yspeed <= 11)
          this.yspeed = -6;
        else if (this.yspeed > 11 && this.jumped_prev_frame)
          this.yspeed = -6;
        else
        {
          this.yspeed = -9;
          this.jumped_prev_frame = false;
        }
      }

      //Animation//
      if (this.floorwait < this.fwmax*0.5)
        SetPersonFrame(this.name, 1);
      else
        SetPersonFrame(this.name, 2);
    }

    //Collission detection//
    for (var i = 0; i != Math.floor(this.yspeed); i+=this.ydir)
    {
      if (IsPersonObstructed(this.name, this.x, this.y+this.ydir) && this.ydir == -1) this.yspeed = 0;
      if (IsPersonObstructed(this.name, this.x, this.y+this.ydir)) break;
      this.y+=this.ydir;
      if (this.jumped_prev_frame) this.y += this.ydir;
    }


    //HORIZONTAL MOVEMENT
    //It's affected by the jumps, so calculated after the jumps.
    if (this.inair)
    {
      if (INPUT.pressed("left") && this.xspeed > -this.max_friction)
        this.xspeed--;
      else if (INPUT.pressed("right") && this.xspeed < this.max_friction)
        this.xspeed++;

      //Nothing pressed
      else if (!INPUT.pressed("left") && !INPUT.pressed("right") && !this.floating)
      {
        if (this.xspeed > 0) this.xspeed--;
        else if (this.xspeed < 0) this.xspeed++;
      }

      if (this.xspeed > 0) this.xdir = 1;
      else if (this.xspeed < 0) this.xdir = -1;


      //Collission detection//
      for (var i = 0; i != this.xspeed; i+=this.xdir)
      {
        if (IsPersonObstructed(this.name, this.x+this.xdir, this.y)) { this.xspeed = 0; break; }
        this.x+=this.xdir/2;
      }

      //Animation//
      if (this.xdir == 1) SetPersonDirection(this.name, "east");
      else SetPersonDirection(this.name, "west");


      //REPOSITION THE CHARACTER FOR REALS
      SetPersonX(this.name, this.x);
      SetPersonY(this.name, this.y);

      //Bubble powerup.
      if (this.powerups.bubble)
      {
        SetPersonX("bubble", this.x);
        SetPersonY("bubble", this.y);
      }
    }
  }



  /** Check if the player is touching anything and respond accordingly. **/
  this.checkOverlap = function()
  {
    //Gets to the finish?
    if (this.x > GetLayerWidth(0)*16 || this.x < 0)
    {
      LEVEL.win();
      return;
    }

    //First, check the tiles.
    if (this.x >= 0 && this.x < GetLayerWidth(0)*16 && this.y >= 0 && this.y < GetLayerHeight(0)*16)
    {
      var tilename = GetTileName(GetTile(Math.floor(this.x/16), Math.floor(this.y/16), 0));
      switch (tilename)
      {
        case "hazard":
          if (this.powerups.bubble)
          {
            res.load("explode01.ogg").play(false);
            this.hurt();
            this.enableBubble(false);
            this.x -= this.xdir*5;
            break;
          }
          res.load("explode02.ogg").play(false);
          this.health = 0;
          this.die();
          break;
      }
    }

    //Falls outside the level?
    else if (this.y > GetLayerHeight(0)*16+32)
    {
      res.load("fall01.ogg").play(false);
      this.health = 0;
      this.die();
    }

    //Then, check the sprites.
    if (GetObstructingPerson(this.name, this.x, this.y) != "")
    {
      var otherguy = GetObstructingPerson(this.name, this.x, this.y);
      var otherguytype = GetPersonValue(otherguy, "type");

      if (otherguytype == "enemy" && !GetPersonValue(otherguy, "new"))
      {
        if (this.powerups.bubble)
        {
          this.enableBubble(false);
          this.updateMovement();
          this.xspeed = -this.xdir*10;
          this.yspeed = -6
          this.y -= 5;
        }
        else
          this.hurt();
      }

      else if (otherguytype == "lethal-enemy" && !GetPersonValue(otherguy, "new"))
      {
        if (this.powerups.bubble)
        {
          res.load("explode01.ogg").play(false);
          this.hurt();
          this.enableBubble(false);
        }
        else
        {
          res.load("explode02.ogg").play(false);
          this.die();
        }
      }

      else if (otherguytype == "platform")
      {
        this.yspeed = -6;
        if (INPUT.pressed("jump")) this.yspeed = -10;
        this.floattime = 0;
        this.floated_this_jump = false;
      }

      else if (otherguytype == "nojumpkey")  //Disallows high jumping.
      {
        this.yspeed = -6;
        this.floattime = 0;
        this.floated_this_jump = false;
      }

      else if (otherguytype == "powerup")
      {
        LEVEL.music.stop();
        LEVEL.powerupmusic.play(false);
        
        if (otherguy.substring(0, 6) != "bubble")
          PLAYER.powerups.got.push(LEVEL.mapfile);

        if (otherguy.substring(0, 5) == "hplus")
        {
          this.maxhealth++;
          this.health = this.maxhealth;
        }

        else if (otherguy.substring(0, 7) == "bubble_")  //Avoid finding "bubbleprotect".
          this.enableBubble(true);

        else if (otherguy.substring(0, 7) == "feather")
          this.powerups.longer_float++;

        else if (otherguy.substring(0, 6) == "hfloat")
          this.powerups.high_float = true;

        DestroyPerson(otherguy);
      }

      else if (otherguy.substring(0, 7) == "cupcake")
      {
        this.cupcakes++;
        this.score += 100;
        DestroyPerson(otherguy);
        LEVEL.gotcupcakes.push(otherguy);
        res.load("blip02.ogg").stop();
        res.load("blip02.ogg").play(false);
      }

      else if (otherguy.substring(0, 5) == "heart")
      {
        if (this.health == this.maxhealth)
        {
          this.lives++;
          res.load("ding.ogg").play(false);
        }
        else
        {
          this.health+=2;
          res.load("jump02.ogg").stop();
          res.load("jump02.ogg").play(false);
        }
        
        if (this.health > this.maxhealth)
          this.health = this.maxhealth;
        
        this.score += 1000;
        DestroyPerson(otherguy);
      }
    }

    //Last, check the zones to set the help text to nothing if there is none.
    if (AreZonesAt(this.x, this.y, 0))
      ExecuteZones(this.x, this.y, 0);
    else if (LEVEL.helptext != "")
      LEVEL.helptext = "";
  }


  /** Hurt the player. **/
  this.hurt = function()
  {
    if (!this.alreadyhurt)
    {
      res.load("explode04.ogg").play(false);
      this.health--;
      this.alreadyhurt = true;
    }

    if (this.health == 0)
    {
      res.load("explode02.ogg").play(false);
      this.alreadyhurt = false;
      this.die();
    }
    else
    {
      SetRenderScript("LEVEL.renderHUD(); PLAYER.hurtUpdate();");
      this.xspeed = -this.xspeed*2;
      
      if (this.yspeed >= 0)
      {
        this.yspeed = -6;
        this.y -= 5;
      }
      else
      {
        this.yspeed = 1;
        this.y += 5;
      }
      
      this.updateMovement();
    }
  }


  /** UpdateScript for player hurtin'. **/
  this.hurtUpdate = function()
  {
    if (!this.hurtcounter) this.hurtcounter = 0;
    this.hurtcounter++;
    SetPersonFrame(this.name, 8);
    SetPersonVisible(this.name, this.hurtcounter%2);

    if (this.hurtcounter > 45)
    {
      SetRenderScript("LEVEL.renderHUD();");
      SetPersonVisible(this.name, true);
      this.hurtcounter = 0;
      this.alreadyhurt = false;
    }
  }


  /** Kill the player. **/
  this.die = function()
  {
    LEVEL.music.stop();
    if (this.lives == 0)
      res.load("music/gameover.ogg").play(false);
    else
      res.load("music/death.ogg").play(false);

    this.lives--;
    SetPersonVisible(this.name, false);
    SetRenderScript("LEVEL.renderHUD(); PLAYER.dieUpdate();");
    SetUpdateScript("");
    SetPersonFrame(this.name, 8);
  }


  /** UpdateScript for player killin'. **/
  this.dieUpdate = function()
  {
    if (!this.diecounter) this.diecounter = 0;
    this.diecounter++;
    SetPersonVisible(this.name, this.diecounter%2);
    if (this.diecounter > 40)
    {
      this.diecounter = 0;
      SetPersonVisible(this.name, false);
      SetRenderScript("LEVEL.renderHUD();");
      LEVEL.dieScreen();
      this.resetMovement();
    }
  }


  this.enableBubble = function(enable)
  {
    this.powerups.bubble = enable;
    SetPersonVisible("bubble", enable);
  }


  /** Take a wild guess. **/
  this.debug = function()
  {
    LEVEL.font.drawTextBox(10, 10, 300, 200, 0,
      "x: " + this.x + "; xspeed: " + this.xspeed + "; xdir: " + this.xdir +
      "\ny: " + this.y + "; yspeed: " + this.yspeed + "; ydir: " + this.ydir +
      "\ninair: " + this.inair + "; jumped_prev_frame: " + this.jumped_prev_frame +
      "\nfloating: " + this.floating + "; floated_this_jump: " + this.floated_this_jump + "; floattime: " + this.floattime
      );
  }
}