Math.curvyRandom = function(deviation) {
  return deviation * ((Math.random()*2-1)+(Math.random()*2-1)+(Math.random()*2-1));
}

var Game = new Class({
  idealFPS: 60, FPS: 50, minFPS: 30,
  maxCpuUsage: 0.80, // throttle to only use 80% of available cpu time - note, browser will also use some cpu in order to draw to screen and do other browsery things, usually about 20% more
  lastDraw: null,
  mouse: {x: 0, y: 0, down: false},
  mouseDown: false,
  level: false,
  levelName: '1, Intro',
  //levelName: '2, Volcano',
  loadingLevel: false, // stores name of level currently being loaded
  debug: false,
  paused: false,
  progressFade: 0,
  world: null,
  worldRunloop: null,
  physicsIterations: 10,
  gravity: new b2Vec2(0, +200),
  font: false,
  
  
  start: function(canvas) {
    this.canvas = $(canvas); this.displayCtx = this.canvas.getContext('2d');
    //this.internalCanvas = this.canvas.clone(); this.ctx = this.internalCanvas.getContext('2d');
    //this.displayCtx.globalCompositeOperation = 'copy';
    this.lastDraw = (new Date()).getTime();
    this.saucer = new Game.Saucer();
    this.mouse.x = this.canvas.width / 2;
    
    this.ctx = this.displayCtx;
    // setup the level
    this.setLevel(this.levelName);
    
    this.font = new Game.SpriteFont('images/handfont.png', [
      '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '%', '+', '-', '/', '⨉',
      '☺', '☹', '✭', '&shoe;', '!', '?', '‽', '$', '€', '&mouse;', ',', '.', ' ',
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
      'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
      'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
      't', 'u', 'v', 'w', 'x', 'y', 'z', '=', ':']);
    
    // fancy cpu-power aware loopy handler thing
    var self = this, runloop = function() {
      var runtime = 0;
      if (!self.paused) {
        self.loop();
        runtime = (new Date()).getTime() - self.now;
        if (runtime / (self.sec * 1000) > self.maxCpuUsage) self.FPS = (self.FPS - 1).limit(self.minFPS, self.idealFPS);
        if (runtime / (self.sec * 1000) < self.maxCpuUsage && self.FPS < self.idealFPS) self.FPS += 1;
        if (self.FPS < self.minFPS) self.FPS = self.minFPS;
        
      }
      runloop.delay((1000 / self.FPS) - runtime);
    };
    
    runloop();
    //self.loop.periodical(1000 / self.FPS, self);
    
    var self = this;
    this.canvas.addEvents({
      mousemove: function(evt) {
        var pos = self.canvas.getPosition();
        self.mouse.x = evt.page.x - pos.x;
        self.mouse.y = evt.page.y - pos.y;
      },
      mousedown: function() { self.mouse.down = self.mouseDown = true },
      mouseup: function() { self.mouse.down = self.mouseDown = false },
      mouseout: function() { self.mouse.down = self.mouseDown = false }
    });
    // handle the pausing
    var windowEvents = {
      blur: function() { self.setPaused(true) },
      focus: function() { self.setPaused(false) },
      keydown: function(e) { if (e.key == 'd') self.debug = true; },
      keyup: function(e) { if (e.key == 'd') self.debug = false; }
    };
    // Disabled for now
    window.addEvents(windowEvents);
    document.addEvents(windowEvents);
  },
  
  loop: function() {
    // seconds since last draw event happened (usually a 0.0... sort of number)
    this.now = (new Date()).getTime();
    this.sec = (this.now - this.lastDraw) / 1000;
    var ctx = this.ctx, self = this;
    
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    ctx.save();
      
      if (this.level && this.level.loaded) {
        // run the physics
        var worldSec = (1 / self.FPS);
        if (this.world && this.level.loaded) self.world.Step(worldSec, this.physicsIterations);
        
        this.saucer.target = this.mouse;
        this.saucer.drawBelow(this);
        ctx.save(); (this.level.draw || $empty).run(this, this.level); ctx.restore();
        this.saucer.drawAbove(this);
      }
      
      // do level loading display
      if (!this.level || this.level.loaded == false || this.progressFade > 0) {
        if (this.level) {
          var progress = 0, part = 1 / this.level.images.getLength();
          
          this.level.loaded = this.level.images.getValues().every(function(img) {
            if (img.complete) progress += part;
            return img.complete;
          });
        } else var progress = 0;
        
        // display the progress...
        ctx.save();
          ctx.globalAlpha = this.progressFade;
          ctx.save();
            ctx.fillStyle = 'white';
            ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
          ctx.restore();
          
          this.darken();
          this.progressBar(this.canvas.width / 2, this.canvas.height / 2, 200, 25, progress);
          // use an empty progress bar to contain the level name...
          var name = this.loadingLevel || this.levelName, size = this.font.measure(name, 25);
          if (size) {
            this.progressBar(this.canvas.width / 2, this.canvas.height / 3, size.width, size.height, 0);
            this.font.draw(name, size.height, (this.canvas.width / 2) - (size.width / 2) + (size.height / 4), (this.canvas.height / 3) - (size.height / 2));
          }
          
          if (this.level.loaded) this.progressFade = (this.progressFade - (this.sec / 0.3)).limit(0, 1);
        ctx.restore();
      }
      
      // print the mouse coords for aid in building stuff
      if (this.debug) this.font.draw('&mouse;=' + this.mouse.x + '⨉' + this.mouse.y + ' fps=' + this.FPS, 28, 5, 0);
    ctx.restore();
    
    //this.displayCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    //this.displayCtx.drawImage(this.internalCanvas, 0, 0);
    //this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    this.lastDraw = this.now;
  },
  
  // setup a new level
  setLevel: function(level) {
    if (Game.Levels[level]) return this.finallySetLevel(level);
    // Store the name of the level we're loading, incase several load's overlap
    this.loadingLevel = level; this.level = false; this.progressFade = 1.0;
    
    var url = 'levels/' + level.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '/gameplay.js';
    // fix cache issues:
    url += '?noCache=' + Math.round(Math.random() * 999999);
    //new Request({ url: url, method: 'get', evalResponse: true }).send();
    document.head.adopt(new Element('script', {'src': url}));
    
    // TODO: Draw something while the script loads...
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  },
  
  // called by the gameplay.js files in the levels to install themselves
  installLevel: function(level, object) {
    Game.Levels[level] = object;
    if (this.loadingLevel == level) { this.loadingLevel = false; this.finallySetLevel(level); }
  },
  
  // the real guts of setLevel
  finallySetLevel: function(level) {
    var self = this;
    // create the new Box2d World
    var worldBox = new b2AABB();
    worldBox.minVertex.Set(-1000, -1000);
    worldBox.maxVertex.Set(+1000, +1000);
    
    var doSleep = true; // let unmoving objects skip some fancy maths for speed
    this.world = new b2World(worldBox, this.gravity, doSleep);
    
    var oldImages = (this.levelName == level) ? this.level.images : false;
    
    // setup the level object
    this.levelName = level;
    this.level = Hash.extend($unlink(Game.DefaultLevel), Game.Levels[level]);
    this.level.loaded = false;
    this.level.urlPrefix = 'levels/' + level.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '/';
    this.level.images = oldImages || (new Hash(this.level.loadImages || {})).map(function(url) {
      return new Element('img', {src: self.level.urlPrefix + url});
    });
    this.level.start(this);
  },
  
  setPaused: function(paused) {
    if (this.paused == paused) return;
    this.paused = paused;
    if (this.paused) {
      var ctx = this.ctx, self = this;
      this.darken();
      
      ctx.save();
        ctx.translate(this.canvas.width / 2, this.canvas.height / 2);
        ctx.beginPath();
          ctx.arc(0, 0, 30, 0, Math.PI * 2, true);
        ctx.strokeStyle = 'white'; ctx.lineWidth = 2;
        ctx.fill();
        ctx.stroke();
        ctx.fillStyle = 'white';
        ctx.fillRect(-13, -15, 8, 30);
        ctx.fillRect(+5, -15, 8, 30);
      ctx.restore();
    } else {
      // bring it up to date so it doesn't explode from giant gap between frames
      this.lastDraw = (new Date()).getTime();
    }
  },
  
  darken: function() {
    var ctx = this.ctx, self = this, gap = 8; //px
    ctx.save();
      ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
      ctx.beginPath();
      (this.canvas.width * 2 / gap).times(function(i) {
        ctx.translate(+gap, 0);
        ctx.moveTo(0, 0);
        ctx.lineTo(-self.canvas.width, self.canvas.height);
      });
      ctx.stroke();
    ctx.restore();
  },
  
  // draw a progress bar, width as in pixels, progress as in 0.0-1.0 float
  progressBar: function(x, y, width, height, progress) {
    var ctx = this.ctx, self = this, gap = 3;
    if (!this.progressBarPath) this.progressBarPath = function(width, height) {
      ctx.beginPath();
        ctx.moveTo(height / 2, 0);
        ctx.lineTo(width - (height / 2), 0);
        ctx.arc(width - (height / 2), height / 2, height / 2, Math.PI * 1.5, Math.PI * 0.5, false);
        ctx.lineTo(height / 2, height);
        ctx.arc(height / 2, height / 2, height / 2, Math.PI * 0.5, Math.PI * 1.5, false);
      ctx.closePath();
    };
    
    ctx.save();
      // draw the outer container line
      ctx.lineWidth = 2;
      ctx.translate(x - (width / 2), y - (height / 2));
      this.progressBarPath(width, height);
      ctx.fillStyle = 'white';
      ctx.strokeStyle = 'black';
      ctx.fill();
      ctx.stroke();
      
    ctx.restore();
    ctx.save();
    
      // set the clip area to be the bar innards
      ctx.translate(x - (width / 2) + gap, y - (height / 2) + gap);
      this.progressBarPath(width - (gap * 2), height - (gap * 2));
      ctx.clip();
      
      // and finally, fill it
      ctx.fillRect(0, 0, (width - (gap * 2)) * progress.limit(0, 1), height - (gap * 2));
    ctx.restore();
  }
});


// Fancy Pants Spritefont of Jenna's handwriting
Game.SpriteFont = new Class({
  sizes: {}, overlap: 12, // px
  
  initialize: function(url, mapping) {
    this.image = new Element('img', {src: url});
    this.characters = mapping;
    this.allCharsRegexp = new RegExp('(' + mapping.map(function(chr) {
      return chr.escapeRegExp();
    }).join('|') + ')', 'g');
  },
  
  prepared: function(size) {
    if (!this.image.complete) return false;
    if (!this.sizes[size.toString()]) this.renderSize(size);
    return true;
  },
  
  draw: function(text, size, x, y) {
    if (!this.prepared(size)) return;
    
    var sourceImages = this.sizes[size.toString()], height = sourceImages.height;
    var ctx = lifter.ctx, self = this, overlap = Math.round(this.overlap * (size / this.image.height));
    var charWidth = sourceImages.width, index = 0;
    
    text.replace(this.allCharsRegexp, function(match) {
      var symbol = sourceImages[match.toString()];
      if (symbol) {
        ctx.drawImage(symbol, (x || 0) + (index * (charWidth - overlap)), y || 0);
        index += 1;
      } else if (window.console) console.log('Ignored ' + JSON.encode(match.toString()));
      return match[0];
    });
  },
  
  measure: function(text, size) {
    if (!this.prepared(size)) return;
    var scaled = this.sizes[size];
    var charCount = 0;
    text.replace(this.allCharsRegexp, function() { charCount++; });
    var width = scaled.width * charCount;
    return {width: width, height: scaled.height};
  },
  
  renderSize: function(size) {
    if (!this.image.complete) throw new Error("Image not available yet for font.renderSize");
    var chars = {}, srcWidth = this.image.width / this.characters.length, srcHeight = this.image.height;
    var height = size, width = srcWidth * (size / this.image.height), image = this.image;
    if (srcWidth != Math.round(srcWidth)) throw new Error("Font Charset or Width incorrect");
    
    this.characters.each(function(symbol, index) {
      var position = index * width;
      var microCanvas = new Element('canvas', {'width': Math.ceil(width), 'height': Math.ceil(height)});
      var ctx = microCanvas.getContext('2d');
      ctx.drawImage(image, index * srcWidth, 0, srcWidth, srcHeight,
                           0, 0, width, height);
      chars[symbol] = microCanvas;
    });
    
    var sizeStr = size.toString();
    this.sizes[sizeStr] = chars;
    this.sizes[sizeStr].width = width;
    this.sizes[sizeStr].height = height;
  }
});


// The player's saucer
Game.Saucer = new Class({
  flyingSpeed: 6,
  spinFraction: 678,
  
  x: -50, y: 50, angle: 0,
  target: {x: 0, y: 50},
  box: {t: -9, b: +4.5, l: -20, r: +20},
  beam: false,
  beamWidth: 60,
  beamAngle: 0 - (Math.PI / 2),
  beamPower: 80, // how many pixels to move the item up per second per second (works with momentum)
  beamStartRadius: 3, // how many pixels wide (in half) to start the beam as
  battery: 1.0, // the amount of power remaining for beams
  feet: 3,
  liveStyle: {
    beamWidth: 0,
    beamHeight: null,
  },
  
  translate: function(ctx) { ctx.translate(this.x, this.y); /*ctx.scale(3, 3);*/ },
  
  // draw the ship
  drawAbove: function(game) {
    var ctx = lifter.ctx;
    ctx.save();
    
    // move to where we want to draw it
    this.translate(ctx);
    
    // draw the capsule thing on top
    ctx.lineWidth = 1.5; ctx.fillStyle = 'white';
    ctx.beginPath();
      ctx.arc(0, -3, 5, Math.PI * 2.2, Math.PI * 0.8, true);
    ctx.fill();
    ctx.stroke();
    
    // and the little reflection corner on the capsule too!
    ctx.beginPath(); ctx.fillStyle = 'black';
      ctx.arc(0, -3, 3.5, Math.PI * -0.1, Math.PI * -0.5, true);
    ctx.closePath();
    ctx.fill();
    
    // draw body of the ship
    ctx.beginPath();
      ctx.moveTo(-20, 0);
      ctx.quadraticCurveTo(-15, -3, -5, -4);
      ctx.quadraticCurveTo(0, -2, +5, -4);
      ctx.quadraticCurveTo(+15, -3, +20, 0);
      ctx.quadraticCurveTo(0, +5, -20, 0);
    ctx.closePath();
    ctx.fill();
    
    // draw fancy lights spinning on the edge
    var lights = 5, singleFraction = this.spinFraction / lights;
    ctx.save();
    ctx.fillStyle = 'white';
    for (var light = 1; light <= lights; light++) {
      var time = game.now + (singleFraction * light);
      var floatPos = ((time % this.spinFraction / this.spinFraction) * 2) - 1;
      var pos = Math.sin(floatPos * Math.PI / 2) * 18;
      ctx.beginPath();
        ctx.arc(pos, -0.3, 0.75, 0, Math.PI * 2, true);
      ctx.fill();
    }
    ctx.restore();
    
    // draw the three feet
    var feet = this.feet, singleFraction = this.spinFraction / feet;
    ctx.save();
    for (var foot = 1; foot <= feet; foot++) {
      var time = game.now + (singleFraction * foot);
      var pos = Math.sin((time % this.spinFraction / this.spinFraction) * Math.PI * 2) * 7;
      ctx.beginPath();
        ctx.arc(pos, +3, 1.5, 0, Math.PI * 2, true);
      ctx.fill();
    }
    ctx.restore();
    
    ctx.restore();
  },
  
  // Calculate position and draw beam and stuff
  drawBelow: function(game) {
    var ctx = game.ctx;
    ctx.save();
    
    // move saucer if needed
    this.x += (this.target.x - this.x) * (this.flyingSpeed * game.sec);
    
    // move to where we want to draw it
    this.translate(ctx);
    
    if (this.target.y > this.y + 30) {
      this.beam = game.mouse.down;
      if (this.beam) this.battery -= 0.20 * game.sec;
      if (this.battery <= 0) this.beam = false;
      if (this.beam) this.beamAngle = Math.atan2(this.y - this.target.y, this.x - this.target.x);
    } else this.beam = false;
    
    this.battery += 0.15 * game.sec;
    this.battery = this.battery.limit(0, 1);
    
    var target = this.beam ? this.beamWidth : 0, ls = this.liveStyle;
    ls.beamWidth += (target - ls.beamWidth) * (10 * game.sec);
    
    if (ls.beamWidth > 0.1) { ctx.save();
      ctx.rotate(this.beamAngle + (Math.PI * 0.5));
      ctx.beginPath();
        if (!this.liveStyle.beamHeight) this.liveStyle.beamHeight = game.canvas.width + game.canvas.height;
        ctx.moveTo(- (this.beamStartRadius), 0);
        ctx.lineTo(- (this.liveStyle.beamWidth), this.liveStyle.beamHeight);
        ctx.lineTo(+ (this.liveStyle.beamWidth), this.liveStyle.beamHeight);
        ctx.lineTo(+ (this.beamStartRadius), 0);
      ctx.closePath();
      ctx.globalAlpha = 0.3;
      if (this.liveStyle.beamWidth <= 1.0) ctx.globalAlpha *= this.liveStyle.beamWidth;
      ctx.fillStyle = '#80ff00';
      ctx.fill();
    ctx.restore(); }
    
    this.beamUp(game);
    ctx.restore();
    
    this.drawTheBattery(game);
  },
  
  drawTheBattery: function(game) {
    var ctx = game.ctx, statsWidth = 200, padding = 25;
    ctx.save();
      ctx.lineWidth = 2;
      ctx.translate(game.canvas.width - padding, 5);
      ctx.beginPath();
        ctx.moveTo(-30, 0);
        ctx.lineTo(-5,  0);
        ctx.quadraticCurveTo(-8, +5, -5, +10);
        ctx.lineTo(-30, +10);
        ctx.quadraticCurveTo(-33, +5, -30, 0);
        // do the little outward curve on the right side
        ctx.moveTo(-4, 0);
        ctx.quadraticCurveTo(-1, +5, -4, +10);
        ctx.moveTo(-3, +5);
        ctx.arc(-3, +5, 1, 0, Math.PI * 2, false);
      ctx.stroke();
      
      var battWidth = this.battery.limit(0, 1) * 19.5,
          bw = battWidth, w = 28, t = +2, b = +8, i = 2;
      
      if (battWidth > 0) {
        ctx.beginPath();
          ctx.moveTo(-w, t);
          ctx.lineTo(-w + bw, t);
          ctx.quadraticCurveTo(-w + bw - i, +5, (-w) + bw, b);
          ctx.lineTo(-w, b);
          ctx.quadraticCurveTo((-w) - i, +5, -w, t);
        ctx.fillStyle = (this.battery < 0.25) ? 'red' : 'black';
        ctx.fill();
      }
      
      // the red angry no power lines
      if (this.battery <= 0 && game.now % 400 > 200) {
        ctx.beginPath();
          // left side
          ctx.moveTo(-w-10, t ); ctx.lineTo(-w-20, t-3);
          ctx.moveTo(-w-10.5, +5); ctx.lineTo(-w-22, +5 );
          ctx.moveTo(-w-10, b ); ctx.lineTo(-w-20, b+3);
          // right side
          ctx.moveTo(+4, t ); ctx.lineTo(+14, t-3);
          ctx.moveTo(+4.5, +5); ctx.lineTo(+16, +5 );
          ctx.moveTo(+4, b ); ctx.lineTo(+14, b+3);
        ctx.strokeStyle = 'red';
        ctx.stroke();
      }
      
    ctx.restore();
  },
  
  // Because firefox fails so badly at <canvas>, I couldn't just use isPointInPath
  // So I had to write this monstrosity, but it works.
  isThingInBeam: function(thing) {
    var thingPos = thing.getPosition ? thing.getPosition() : thing;
    var angle = this.beamAngle + Math.PI;
    var bx = this.x, by = this.y; // base x/y (saucer position)
    var lx = (Math.cos(angle) * this.liveStyle.beamHeight); // line end x/y
    var ly = (Math.sin(angle) * this.liveStyle.beamHeight);
    var rx = thingPos.x - bx, ry = thingPos.y - by; // relative to saucer
    var distance = Math.sqrt((rx * rx) + (ry * ry)); // distance px (in a round way) from saucer
    var rMaxRadius = this.beamWidth - this.beamStartRadius; // max width minux min width
    var portion = distance / this.liveStyle.beamHeight; // 0-1 along the beam's height
    var radius = (rMaxRadius * portion) + this.beamStartRadius;
    // alter radius to be squished by angle of beam
    radius *= Math.sin(angle);
    // check if thing is within the right bounds
    var within_left = bx + (lx * portion) - radius, within_right = bx + (lx * portion) + radius;
    return (thingPos.x > within_left && thingPos.x < within_right);
  },
  
  beamUp: function(game) {
    game.ctx.save();
    var self = this, best = false;
    game.level.drawOrder.each(function(thing) {
      //if (!thing.box || thing.beamable != true) return;
      if (thing.center.y + thing.box.t < self.y + self.box.b && 
          thing.center.x + thing.box.r > self.x + self.box.l &&
          thing.center.x + thing.box.l < self.x + self.box.r) {
        // Inform thing so it can bounce or whatever
        thing.onHitSaucer(self);
      } else if (self.beam && thing.beamable && self.isThingInBeam(thing)) {
        if (!best || thing.center.y < best.center.y) {
          best = thing;
        }
      }
    });
    if (best) { // do normal levitation
      best.body.WakeUp();
      best.antiGravity();
      best.body.ApplyImpulse(new b2Vec2((this.x - best.center.x) * 40, -400), best.center);
    }
    game.ctx.restore();
  },
});


// In game Doodads
Game.Doodad = new Class({
  box: {t:0,b:0,l:0,r:0},
  liftable: false, rotation: 0,
  
  initialize: function(x, y) {
    var self = this, args = $A(arguments), x = args.shift(), y = args.shift();
    this.center = {x: x, y: y};
    this.setup.run(args, this);
    this.images = lifter.level.images; this.level = lifter.level; this.ctx = lifter.ctx;
    this.game = lifter;
  },
  setup: $empty,
  setSize: function(width, height) {
    this.box.l = -(this.box.r = width / 2);
    this.box.t = -(this.box.b = height / 2);
  },
  getSize: function() { return { width: (-this.box.l)+this.box.r, height: (-this.box.t)+this.box.b }; },
  contains: function(x, y) {
    if (x.center) var x = x.center; // support other doodads and physical things
    if (x.x && x.y) var y = x.y, x = x.x; // support b2Vec2 and stuff like that
    return (
      (x > this.center.x + this.box.l && x < this.center.x + this.box.r) &&
      (y > this.center.y + this.box.t && y < this.center.y + this.box.b)
    );
  },
  
  translate: function() {
    lifter.ctx.translate(this.center.x, this.center.y);
    lifter.ctx.rotate(this.body ? this.body.m_rotation : this.rotation);
  },
  
  draw: $empty
});

// In Game Physics Objects
Game.PhysicalBody = new Class({
  Extends: Game.Doodad,
  
  initialize: function(x, y) {
    var self = this, args = $A(arguments), x = args.shift(), y = args.shift();
    var bodyDef = new b2BodyDef();
    bodyDef.position.Set(x, y);
    this.shapes.run(args, this).each(function(i) {
      bodyDef.AddShape(i);
    });
    this.body = lifter.world.CreateBody(bodyDef);
    this.body.m_linearDamping = 0.99;
    this.body.SetLinearVelocity(new b2Vec2(0, 0));
    this.center = this.body.m_position;
  },
  
  getPosition: function() { return this.body.GetOriginPosition(); },
  
  // calling this cancels out gravity with an equally opposing force
  antiGravity: function() {
    var m = this.body.m_mass;
    this.body.ApplyForce(new b2Vec2((-lifter.gravity.x) * m, (-lifter.gravity.y) * m), this.center);
  },
  
  onHitSaucer: function() {
    var velocity = this.body.GetLinearVelocity();
    this.body.SetLinearVelocity(new b2Vec2(velocity.x, (0-velocity.y) * 0.5));
    this.body.SetCenterPosition(new b2Vec2(this.center.x, lifter.saucer.y + lifter.saucer.box.b + -this.box.t), this.body.m_rotation);
  }
});

// Static Rectangle, used for stuff like the flat ground.. immovable, solid as diamonds
Game.StaticRectangleBody = new Class({
  Extends: Game.PhysicalBody,
  
  shapes: function(width, height) {
    var shape = new b2BoxDef();
    this.size = {width: width, height: height};
    this.box.l = 0 - (this.box.r = width / 2);
    this.box.t = 0 - (this.box.b = height / 2);
    shape.extents.Set(this.box.r, this.box.b);
    shape.density = 0;
    return [shape];
  },
  draw: function() {
    if (lifter.debug) {
      this.translate();
      lifter.ctx.fillStyle = 'red';
      lifter.ctx.fillRect(this.box.l, this.box.t, this.box.r * 2, this.box.b * 2);
    }
  }
});

Game.SimpleFloor = new Class({
  Extends: Game.StaticRectangleBody,
  
  initialize: function(relHeight) {
    this.parent(lifter.canvas.width / 2, lifter.canvas.height + (relHeight / 2), lifter.canvas.width, -relHeight);
  }
});

Game.TestSquare = new Class({
  Extends: Game.PhysicalBody,
  
  shapes: function() {
    var shape = new b2BoxDef();
    shape.extents.Set(1, 1);
    shape.density = 1.0; shape.friction = 0.3;
    return [shape];
  },
  
  draw: function() {
    var ctx = lifter.ctx;
    this.translate(ctx);
    ctx.fillRect(-1, -1, +2, +2);
  }
});

Game.Ball = new Class({
  Extends: Game.PhysicalBody,
  box: {t: -5, b: +5, l: -5, r: +5},
  beamable: true,
  density: 1.0,
  
  initialize: function(colour, x, y) {
    this.colour = colour; this.parent(x, y);
    this.body.m_linearDamping = 0.985;
  },
  
  shapes: function() {
    // TODO: Create a ball shape
    var shape = new b2CircleDef();
    shape.radius = this.box.b;
    shape.density = this.density; shape.friction = 0.3;
    return [shape];
  },
  
  draw: function(game) {
    var ctx = game.ctx;
    ctx.save();
    ctx.fillStyle = this.colour;
    ctx.beginPath();
      this.translate(ctx);
      ctx.arc(0, 0, 5, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  },
});

// The magic ball you suck in to your saucer to move to a different level
Game.ContinueBall = new Class({
  Extends: Game.Ball,
  //density: 0.3,
  
  initialize: function(x, y, nextLevel) {
    this.nextLevel = nextLevel || lifter.level.nextLevel;
    this.setSize(20, 20);
    this.parent('#6666ff', x, y);
  },
  
  draw: function() {
    var ctx = lifter.ctx;
    ctx.save();
    
    this.antiGravity();
    
    // slow down near saucer
    //if (Math.abs(game.saucer.y - this.y) < 10) this.momentum.y *= 0.9;
    //if (game.saucer.y >= this.y) { this.momentum.y = 0; this.y = game.saucer.y; }
    //this.momentum.y *= 0.99 * (1 - game.sec); // give it friction
    if (this.center.y <= lifter.saucer.y) { this.body.SetAngularVelocity(0) }
    
    // the pulsing ring
    ctx.save();
      var f = lifter.now % 500 / 500;
      ctx.beginPath();
      ctx.globalAlpha = 1 - f;
      ctx.arc(this.center.x, this.center.y, 9 + (f * 7), 0, Math.PI * 2, true);
      ctx.strokeStyle = this.colour;
      ctx.lineWidth = 2;
      ctx.closePath();
      ctx.stroke();
    ctx.restore();
    
    // the circle
    ctx.beginPath();
    ctx.arc(this.center.x, this.center.y, 10, 0, Math.PI * 2, true);
    ctx.fillStyle = this.colour;
    ctx.closePath();
    ctx.fill();
    
    ctx.strokeStyle = 'white';
    this.logo(ctx);
    
    ctx.restore();
  },
  
  logo: function(ctx) {
    ctx.beginPath();
      var x = this.center.x, y = this.center.y;
      ctx.moveTo(x - 5, y);
      ctx.arc(x - 5, y, 1, 0, Math.PI * 2, true);
      ctx.moveTo(x, y);
      ctx.arc(x + 0, y, 1, 0, Math.PI * 2, true);
      ctx.moveTo(x + 3, y + 3);
      ctx.arc(x + 3, y, 3, Math.PI * 0.5, Math.PI * 1.5, true);
    ctx.lineWidth = 2;
    ctx.stroke();
  },
  
  onHitSaucer: function(saucer) {
    this.onHitSaucer = $empty;
    lifter.setLevel.delay(10, lifter, this.nextLevel);
  }
});

Game.RetryBall = new Class({
  Extends: Game.ContinueBall,
  
  initialize: function(x, y) {
    this.parent(x, y, lifter.levelName);
    this.colour = '#ff6666';
  },
  
  logo: function(ctx) {
    ctx.beginPath();
      var x = this.center.x, y = this.center.y, radius = 5, arrow = 1.5;
      ctx.moveTo(x, y-radius);
      ctx.lineTo(x, y-radius-arrow);
      ctx.lineTo(x+arrow, y-radius);
      ctx.lineTo(x, y+arrow-radius);
      ctx.lineTo(x, y-radius);
      ctx.arc(x, y, radius, Math.PI * 1.5, Math.PI * -0.2, true);
    ctx.lineWidth = 2;
    ctx.stroke();
  }
});




// level doodads are parts of particular levels you can't move using the craft
/*Game.LevelDoodad = new Class({
  x: 0, y: 0, width: 0, height: 0,
  initialize: function(x, y, w, h) { this.x = x; this.y = y; this.width = w; this.height = h; },
  getSize: function() { return { width: this.width, height: this.height }; },
  getLiveBox: function() { return { l: this.x, r: this.x + this.width, t: this.y, b: this.y + this.height }; },
  run: function(level) { this.level = level; this.hitTest(); this.draw(level); },
  draw: $empty, onImpact: $empty,
  hitTest: function() {
    var self = this;
    this.level.game.stuff.each(function(thing) {
      if (thing.isTouchingThing(self)) self.onImpact(thing);
    });
  }
});

Game.StaticImage = new Class({
  Extends: Game.LevelDoodad,
  image: null,
  initialize: function(x, y, image) {
    this.parent(x, y, false, false); this.image = image;
  },
  draw: function(level) {
    if (!this.image.complete) return;
    level.game.ctx.drawImage(this.image, this.x, this.y, this.width || this.image.width, this.height || this.image.height);
  }
});*/

Game.DefaultLevel = {
  start: $empty,
  drawAbove: $empty,
  drawBelow: $empty,
  drawOrder: [],
  
  drawStuff: function() {
    this.drawOrder.each(function(thing) {
      lifter.ctx.save();
      thing.draw(lifter);
      lifter.ctx.restore();
    });
  }
}

Game.Levels = {};

window.addEvent('load', function() {
  window.lifter = new Game();
  lifter.start('gamey');
});
