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

Object.duplicate = function(src, working) {
  if (!src || !src.constructor) return src;
  switch(true) {
  case src instanceof Array:
  case src.length:
    return Array.prototype.slice.call(src, 0);
  case src instanceof String:
  case src instanceof Number:
  case src instanceof Boolean:
    return src;
  default:
    //if (src.constructor) return src;
    //var thingy = Object.create(src);
    //for (var key in src) {
    //  var duplicated = Object.duplicate(src[key]);
    //  if (duplicated != src[key]) thingy[key] = duplicated;
    //}
    //return thingy;
    var newObj = (src instanceof Array) ? [] : {};
    for (i in src) {
      if (i == 'clone') continue;
      if (src[i] && typeof src[i] == "object") {
        newObj[i] = Object.duplicate(src[i]);
      } else newObj[i] = src[i];
    }
    return newObj;
  }
}

Object.merge = function(from, to, replace) {
  for (key in from) { if (replace || !to[key]) to[key] = from[key]; }
}

Object.keys = function(obj) {
  var keys = []
  for (key in obj) keys.push(key);
  return keys;
}

Object.values = function(obj) {
  var values = []
  for (key in obj) values.push(obj[key]);
  return values;
}

Array.from = function(thing) {
  return Array.prototype.slice.call(thing, 0);
}

Function.empty = function() {};
Function.SkipParent = function(name) {
  return function() {
    var args = Array.prototype.slice.call(arguments, 0);
    args.unshift(name);
    this.callParent.apply(this, args);
  }
}

Object.defineProperty(Array, 'select', { value: function(tester) {
  var selected = []
  for (var i = 0; i < this.length; i++) if (tester(this[i], i)) selected.push(this[i]);
  return selected;
}, configurable: false, enumerable: false});

RegExp.escape = function(word) { return word.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1'); };
Number.prototype.limit = function(min, max) { return Math.min(max, Math.max(min, this)); }

var Class = function(bits) {
  var constructor = function() {
    //Object.duplicate(this.prototype, this); // so they aren't shitty references!
    if ('initialize' in this) this.initialize.apply(this, arguments);
  }
  
  if (!bits.Extends) bits.Extends = Class.Base;
  constructor.prototype = Object.duplicate(bits.Extends.prototype);
  constructor.prototype.parent = bits.Extends.prototype;
  
  for (var key in bits) {
    constructor.prototype[key] = bits[key];
  }
  
  return constructor;
}

Class.Base = {
  prototype: {
    callParent: function selfref() {
      // var args = Array.prototype.slice.call(arguments, 0); name = args.shift();
      // var formerParent = this.parent;
      // this.parent = this.parent.parent;
      // var returned = formerParent[name].apply(this, args);
      // this.parent = formerParent;
      // return returned;
      
      // build an array of the inheritance levels in this linked-list chain
      var args = Array.prototype.slice.call(arguments, 0); name = args.shift();
      var inheritance = [], cursor = this;
      while (cursor != Class.Base.prototype && cursor != cursor.parent) {
        inheritance.push(cursor);
        cursor = cursor.parent;
      }
      //console.log(inheritance.length);
      window.a = selfref.caller;
      window.inheritance = inheritance;
      for (var i = 0; i < inheritance.length; i++) {
        if (inheritance[i][name] != inheritance[i+1][name] && inheritance[i][name] == selfref.caller) {
          return inheritance[i+1][name].apply(this, args);
        }
      }
      
      throw "Couldn't find any parents to call!"
    }
  }
}

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},
  lastClick: {x: 0, y: 0},
  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 Box2D.Common.Math.b2Vec2(0, +200),
  font: false,
  
  
  start: function(canvas) {
    this.canvas = document.getElementById(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; this.runloop = function looper() {
      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;
      }
      setTimeout(looper, (1000 / self.FPS));// - runtime);
    };
    
    this.runloop();
    //self.loop.periodical(1000 / self.FPS, self);
    
    var self = this;
    this.canvas.onmousemove = function(evt) {
      var pos = {x: self.canvas.offsetLeft, y: self.canvas.offsetTop};
      self.mouse.x = evt.pageX - pos.x;
      self.mouse.y = evt.pageY - pos.y;
    }
    this.canvas.onmousedown = function() { self.mouse.down = self.mouseDown = true }
    this.canvas.onmouseup = function() { self.mouse.down = self.mouseDown = false }
    this.canvas.onmouseout = function() { self.mouse.down = self.mouseDown = false }
    
    // Make iPads happy little multitouches!
    this.canvas.ontouchstart = this.canvas.ontouchend = this.canvas.ontouchmove = function(evt) {
      self.mouse.down = (evt.targetTouches.length > 0);
      if (!self.mouse.down) return;
      var pos = {x: self.canvas.offsetLeft, y: self.canvas.offsetTop};
      self.mouse.x = evt.targetTouches[0].clientX - pos.x;
      self.mouse.y = evt.targetTouches[0].clientY - pos.y;
      evt.preventDefault();
    };
    
    // handle the pausing
    window.onblur = function() { self.setPaused(true) }
    window.onfocus = function() { self.setPaused(false) }
    window.onkeydown = function(e) {
      if (String.fromCharCode(e.keyCode) == 'D') self.debug = true;
    }
    window.onkeyup = function(e) {
      if (String.fromCharCode(e.keyCode) == 'D') self.debug = false;
      // polygon making stuff
      if (String.fromCharCode(e.keyCode) == 'P') self.startPolygon();
      if (String.fromCharCode(e.keyCode) == 'O') self.setPolyOrigin();
      if (String.fromCharCode(e.keyCode) == 'I') self.addPointToPoly();
    }
  },
  
  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) {
        this.saucer.preStepUpdate(this);
        
        // run the physics
        var worldSec = this.sec;
        if (this.world && this.level.loaded) {
          this.world.Step(worldSec, this.physicsIterations, this.physicsIterations);
          if (this.debug) {
            ctx.save();
            this.world.DrawDebugData();
            ctx.restore();
            ctx.globalAlpha = 0.3;
          }
          this.world.ClearForces();
        }
        
        this.saucer.target = this.mouse;
        ctx.save();
        (this.level.drawBelow || Function.empty).call(this.level, this);
        this.saucer.drawBelow(this);
        (this.level.draw || Function.empty).call(this.level, this);
        this.saucer.drawAbove(this);
        (this.level.drawAbove || Function.empty).call(this.level, this);
        ctx.restore();
      }
      
      // do level loading display
      if (!this.level || this.level.loaded == false || this.progressFade > 0) {
        if (this.level) {
          var progress = 0, part = 1 / Object.keys(this.level.images).length;
          
          this.level.loaded = Object.values(this.level.images).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
    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);
    
    if (this.debug)
      this.font.draw('&mouse;=' + this.mouse.x + '⨉' + this.mouse.y + ' fps=' + this.FPS, 28, 5, 0);
    
    this.lastDraw = this.now;
    if (this.mouse.down) this.lastClick = this.mouse;
  },
  
  // 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();
    var script = document.createElement('script');
    script.src = url;
    document.head.appendChild(script);
    
    // 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;
    var doSleep = true; // let unmoving objects skip some fancy maths for speed
    this.world = new Box2D.Dynamics.b2World(this.gravity, doSleep);
    
    var dbgDraw = new Box2D.Dynamics.b2DebugDraw;
    dbgDraw.SetSprite(this.displayCtx);
    dbgDraw.SetDrawScale(1.0);
    dbgDraw.SetFillAlpha(0.3);
		dbgDraw.SetLineThickness(1.0);
		dbgDraw.SetFlags(Box2D.Dynamics.b2DebugDraw.e_shapeBit | Box2D.Dynamics.b2DebugDraw.e_jointBit);
		this.world.SetDebugDraw(dbgDraw);
		
		var notify = function(listener, notice, other) {
		  var ud; if (listener && (ud = listener.GetUserData()) && ud[notice]) ud[notice].call(ud, other);
	  }
		this.world.SetContactListener({
		  BeginContact: function(contact) {
		    //console.log(contact);
		    notify(contact.GetFixtureA(), 'onBeginContact', contact.GetFixtureB());
		    notify(contact.GetFixtureB(), 'onBeginContact', contact.GetFixtureA());
	    },
	    EndContact: function(contact) {
	      notify(contact.GetFixtureA(), 'onEndContact', contact.GetFixtureB());
	      notify(contact.GetFixtureB(), 'onEndContact', contact.GetFixtureA());
      },
      PreSolve: Function.empty,
      PostSolve: Function.empty
    });
    
    var oldImages = (this.levelName == level) ? this.level.images : false;
    
    // setup the level object
    this.levelName = level;
    var levelObject = Object.duplicate(Game.Levels[level]);
    Object.merge(Game.DefaultLevel, levelObject, false);
    levelObject.loaded = false;
    levelObject.urlPrefix = 'levels/' + level.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '/';
    levelObject.images = {};
    for (var spriteName in levelObject.loadImages) {
      var image = levelObject.images[spriteName] = document.createElement('img');
      image.src = levelObject.urlPrefix + levelObject.loadImages[spriteName];
    }
    
    this.level = levelObject;
    this.saucer.initPhysics(this);
    this.saucer.reset();
    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();
      for (var i = 0; i < (this.canvas.width * 2 / gap); 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();
  },
  
  // polygon designer helpers for Jenna
  setPolyOrigin: function() { this.polygon.push(this.lastClick.x, this.lastClick.y); this.polyorigin = Object.duplicate(this.lastClick); },
  startPolygon: function() { this.polygon = []; },
  addPointToPoly: function() { this.polygon.push(this.lastClick.x - this.polyorigin.x, this.lastClick.y - this.polyorigin.y); },
  exportPolygon: function() { console.log(this.polygon.join(", ")); },
});


// Fancy Pants Spritefont of Jenna's handwriting
Game.SpriteFont = new Class({
  sizes: {}, overlap: 12, // px
  
  initialize: function(url, mapping) {
    this.image = new Image();
    this.image.src = url;
    this.characters = mapping;
    this.allCharsRegexp = new RegExp('(' + mapping.map(function(chr) {
      return RegExp.escape(chr);
    }).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.forEach(function(symbol, index) {
      var position = index * width;
      var microCanvas = document.createElement('canvas');
      microCanvas.width = Math.ceil(width);
      microCanvas.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,
  saucer: true,
  
  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,
  },
  levitatingThings: [], // things we're currently lifting
  thingsInBeam: [],
  
  translate: function(ctx) { ctx.translate(this.x, this.y); /*ctx.scale(3, 3);*/ },
  reset: function() { this.x = -50; this.y = 50; },
  
  // create any physics things in Box2D
  initPhysics: function(game) {
    var vec = Box2D.Common.Math.b2Vec2, beamHeight = game.canvas.width + game.canvas.height, beamVertices = [
      new vec(+ this.beamStartRadius, 0), // top right
      new vec(+ this.beamWidth, beamHeight), // bottom right
      new vec(- this.beamWidth, beamHeight), // bottom left
      new vec(- this.beamStartRadius, 0)
    ];
    
    var bodyDef = new Box2D.Dynamics.b2BodyDef;
    bodyDef.type = Box2D.Dynamics.b2Body.b2_staticBody
    this.beamSensor = game.world.CreateBody(bodyDef);
    
    var fixture = new Box2D.Dynamics.b2FixtureDef;
    fixture.isSensor = true;
    fixture.shape = this.beamShape = new Box2D.Collision.Shapes.b2PolygonShape.AsArray(beamVertices, beamVertices.length);
    fixture.filter.categoryBits = 0x00FF
    fixture.userdata = this;
    this.beamSensor.CreateFixture(fixture);
    this.game = game;
    
    // Build the hull shape
    var hullVertices = [
      new vec(-20, 0),
      new vec(-5, -4),
      new vec(+5, -4),
      new vec(+20, 0),
      new vec(+5, +4),
      new vec(-5, +4)
    ];
    
    var bodyDef = new Box2D.Dynamics.b2BodyDef;
    bodyDef.type = Box2D.Dynamics.b2Body.b2_kinematicBody;
    bodyDef.fixedRotation = true;
    bodyDef.userData = this;
    var fixture = new Box2D.Dynamics.b2FixtureDef;
    fixture.shape = new Box2D.Collision.Shapes.b2PolygonShape.AsArray(hullVertices, hullVertices.length);
    fixture.userData = this;
    this.hull = game.world.CreateBody(bodyDef);
    this.hull.CreateFixture(fixture);
  },
  
  preStepUpdate: function(game) {
    // move saucer if needed
    this.x += (this.target.x - this.x) * (this.flyingSpeed * game.sec);
    
    // and the hull for physics
    this.hull.SetPositionAndAngle(new Box2D.Common.Math.b2Vec2(this.x, this.y), 0);
    
    // beam maintenance
    this.beamSensor.SetPositionAndAngle(new Box2D.Common.Math.b2Vec2(this.x, this.y), this.beamAngle + (Math.PI/2));
  },
  
  // 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 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);
  },*/
  
  //isThingInBeam: function(thing) {
  //  this.beamSensor.SetPositionAndAngle(new Box2D.Common.Math.b2Vec2(this.x, this.y), this.beamAngle + (Math.PI/2));
  //},
  
  thingsInBeam: function() {
    //this.beamSensor.SetPositionAndAngle(new Box2D.Common.Math.b2Vec2(this.x, this.y), this.beamAngle + (Math.PI/2));
    var results = [], userdata, contacting, list = this.beamSensor.GetContactList();
    while (list) {
      contacting = this.beamShape.TestPoint(this.beamSensor.GetTransform(), list.other.GetPosition());
      if (contacting && (userdata = list.other.GetUserData()) && userdata.beamable) results.push(userdata);
      list = list.next;
    }
    return results;
  },
  
  beamUp: function(game) {
    game.ctx.save();
    var best = false, things = this.thingsInBeam();
    if (this.beam) {
      best = things.sort(function(a, b) { return a.getPosition().y - b.getPosition().y; })[0];
    } else { // notify any levitating things that they aren't anymore
      this.levitatingThings.forEach(function(thing) { thing.onFinishedLevitating(game); });
      this.levitatingThings = [];
    }
    
    if (best) { // do normal levitation
      //best.body.WakeUp();
      best.antiGravity();
      var center = best.body.GetWorldCenter();
      best.body.ApplyImpulse(new Box2D.Common.Math.b2Vec2((this.x - center.x) * 40, -400), center);
      // fix up levitating things and do events
      if (this.levitatingThings != [best]) {
        this.levitatingThings = [best];
        best.onLevitating(game);
      }
    }
    game.ctx.restore();
  },
});


// In game Doodads
Game.Doodad = new Class({
  box: {t:0,b:0,l:0,r:0},
  liftable: false, rotation: 0, lifting: false,
  onLevitating: function() { this.lifting = true; },
  onFinishedLevitating: function() { this.lifting = false; },
  
  initialize: function(x, y) {
    var self = this, args = Array.from(arguments), x = args.shift(), y = args.shift();
    this.center = {x: x, y: y};
    this.setup.apply(this, args);
    this.images = lifter.level.images; this.level = lifter.level; this.ctx = lifter.ctx;
    this.game = lifter;
  },
  setup: Function.empty,
  setSize: function(width, height) {
    this.box = {
      r: width / 2,
      b: height / 2
    }
    this.box.l = -this.box.r;
    this.box.t = -this.box.b;
  },
  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.rotation);
  },
  
  draw: Function.empty
});

// In Game Physics Objects
Game.PhysicalBody = new Class({
  Extends: Game.Doodad,
  setProperties: Function.empty,
  properties: {type: Box2D.Dynamics.b2Body.b2_dynamicBody},
  contacts: [],
  
  initialize: function() {
    var self = this, args = Array.prototype.slice.call(arguments, 0), x = args.shift(), y = args.shift();
    var bodyDef = new Box2D.Dynamics.b2BodyDef();
    bodyDef.position.x = x;
    bodyDef.position.y = y;
    Object.merge(this.properties, bodyDef, true);
    this.setProperties(bodyDef);
    this.body = lifter.world.CreateBody(bodyDef);
    this.body.SetUserData(this);
    this.fixtures.apply(this, args).forEach(function(i) {
      i.userData = self;
      self.body.CreateFixture(i);
    });
    this.body.m_linearDamping = 0.99;
    this.body.SetLinearVelocity(new Box2D.Common.Math.b2Vec2(0, 0));
    //this.center = {x: 0, y: 0};
    this.callParent('initialize', x, y);
  },
  
  getPosition: function() { return this.body.GetPosition(); },
  
  // calling this cancels out gravity with an equally opposing force
  antiGravity: function() {
    var m = this.body.GetMass();
    this.body.ApplyForce(new Box2D.Common.Math.b2Vec2((-lifter.gravity.x) * m, (-lifter.gravity.y) * m), this.body.GetWorldCenter());
  },
  
  onHitSaucer: function() {
    var velocity = this.body.GetLinearVelocity(), center = this.body.GetWorldCenter();
    this.body.SetLinearVelocity(new Box2D.Common.Math.b2Vec2(velocity.x, (0-velocity.y) * 0.5));
    this.body.SetPosition(new Box2D.Common.Math.b2Vec2(center.x, lifter.saucer.y + lifter.saucer.box.b + -this.box.t), this.body.m_rotation);
  },
  
  onBeginContact: function(fixture) {
    var userdata = fixture.GetUserData() || fixture.GetBody().GetUserData();
    if (userdata && this.contacts.indexOf(userdata) == -1) this.contacts.push(userdata);
    var self = this; if (userdata) setTimeout(function() { self.onHit(userdata) }, 0);
  },
  
  onEndContact: function(fixture) {
    var userdata = fixture.GetUserData(), removal = this.contacts.indexOf(userdata);
    if (userdata && removal > -1) this.contacts.splice(removal, 1);
  },
  
  onHit: Function.empty,
  
  translate: function() {
    var transform = this.body.GetTransform();
    this.game.ctx.translate(transform.position.x, transform.position.y);
    this.game.ctx.rotate(transform.GetAngle());
  },
  
  isTouching: function(test) {
    var cursor = this.body.GetContactList(), userdata;
    while (cursor) {
      userdata = cursor.other.GetUserData();
      if (userdata && ( (typeof test == 'string' && userdata[test]) || userdata == test))
        return true;
      
      cursor = cursor.next;
    }
    return false;
  },
});

Game.PolygonBody = new Class({
  Extends: Game.PhysicalBody,
  friction: 1, sensor: false, density: 0, restitution: 0,
  
//  initialize: Function.SkipParent('initialize'),
  
  fixtures: function() {
    var specification = Array.from(arguments), vertices = [], fixture = new Box2D.Dynamics.b2FixtureDef;
    for (var offset = 0; offset < specification.length; offset += 2) {
      vertices.push(new Box2D.Common.Math.b2Vec2(specification[offset], specification[offset+1]));
    }
    fixture.shape = Box2D.Collision.Shapes.b2PolygonShape.AsArray(vertices, vertices.length);
    fixture.isSensor = this.sensor;
    fixture.friction = this.friction;
    fixture.density = this.density;
    fixture.restitution = this.restitution;
    return [fixture];
  },
  
  draw: Function.empty
});

Game.StaticPolygonBody = new Class({
  Extends: Game.PolygonBody,
  properties: {type: Box2D.Dynamics.b2Body.b2_staticBody},
});

// Static Rectangle, used for stuff like the flat ground.. immovable, solid as diamonds
Game.StaticRectangleBody = new Class({
  Extends: Game.PhysicalBody,
  properties: {type: Box2D.Dynamics.b2Body.b2_staticBody},
  friction: 1.0, sensor: false,
  
//  initialize: Function.SkipParent('initialize'),
  
  fixtures: function(width, height) {
    var fixture = new Box2D.Dynamics.b2FixtureDef();
    fixture.shape = Box2D.Collision.Shapes.b2PolygonShape.AsBox(width / 2, height / 2);
    this.box = {
      t: 0 - (height / 2),
      b: (height / 2),
      l: 0 - (width / 2),
      r: (width / 2)
    };
    fixture.density = 0;
    fixture.friction = this.friction;
    fixture.isSensor = this.sensor;
    return [fixture];
  },
  
  draw: Function.empty
});

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

Game.TestSquare = new Class({
  Extends: Game.PhysicalBody,
  
  fixtures: function() {
    var fixture = new Box2D.Dynamics.b2FixtureDef;
    fixture.shape = Box2D.Collision.Shapes.b2PolygonShape.AsBox(1, 1);
    fixture.density = 0;
    fixture.friction = this.friction;
    return [fixture];
  },
  
  draw: function() {
    var ctx = lifter.ctx;
    this.translate(ctx);
    ctx.fillRect(-1, -1, +2, +2);
  }
});

Game.Ball = new Class({
  Extends: Game.PhysicalBody,
  beamable: true,
  density: 1.0,
  ball: true,
  sensor: false,
  
  initialize: function(colour, x, y) {
    this.colour = colour;
    this.callParent('initialize', x, y);
    this.body.m_linearDamping = 0.985;
  },
  
  fixtures: function(radius) {
    var fixture = new Box2D.Dynamics.b2FixtureDef;
    fixture.density = this.density;
    fixture.friction = 0.3;
    fixture.isSensor = this.sensor;
    fixture.shape = new Box2D.Collision.Shapes.b2CircleShape(radius || 5);
    return [fixture];
  },
  
  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.callParent('initialize', '#6666ff', x, y);
  },
  
  fixtures: function() {
    var fixtures = this.callParent('fixtures');
    fixtures[0].filter.categoryBits = 0x0002;
    return fixtures;
  },
  
  draw: function() {
    var ctx = lifter.ctx, center = this.body.GetWorldCenter();
    ctx.save();
    
    // TODO: Make this object be a kinematic body instead?
    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(center.x, 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(center.x, center.y, 10, 0, Math.PI * 2, true);
    ctx.fillStyle = this.colour;
    ctx.closePath();
    ctx.fill();
    
    ctx.strokeStyle = 'white';
    this.logo(ctx, center.x, center.y);
    
    ctx.restore();
  },
  
  logo: function(ctx, x, y) {
    ctx.beginPath();
      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();
  },
  
  onHit: function(thing) {
    if (!thing.saucer) return;
    this.onHit = Function.empty;
    window.location.hash = this.nextLevel;
    var self = this;
    setTimeout(function() { lifter.setLevel(self.nextLevel) }, 10);
  }
});

Game.RetryBall = new Class({
  Extends: Game.ContinueBall,
  
  initialize: function(x, y) {
    this.callParent('initialize', x, y, lifter.levelName);
    this.colour = '#ff6666';
  },
  
  logo: function(ctx) {
    ctx.beginPath();
      var pos = this.getPosition(), x = pos.x, y = pos.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();
  }
});

Game.Cow = new Class({
  Extends: Game.PhysicalBody,
  box: {t: -10, b: +10, l: -10, r: +10},
  spriteOffsets: {stand1: 0, stand2: 20, stand3: 40, stand4: 60},
  spriteState: 'stand1',
  density: 0.1,
  restitution: 0.2,
  beamable: true,
  cow: true,
  
  fixtures: function() {
    this.actualShape = Game.Ball.prototype.fixtures.call(this, 7)[0];
    this.sensor = true;
    this.sensorShape = Game.Ball.prototype.fixtures.call(this, 20)[0];
    this.sensor = false;
    return [this.actualShape, this.sensorShape];
  },

  initialize: function(sprite, x, y) {
    this.sprite = sprite;
    this.callParent('initialize', x, y);
  },

  draw: function(game) {
    if (this.contacts.length > 0) {
      var angle = this.body.GetAngle();
      this.body.SetAngularVelocity(0 - angle);
    }
    
    this.spriteState = 'stand' + (1 + Math.floor((game.now % 700) / 700 * 4));

    var ctx = game.ctx;
    this.translate(ctx);
    ctx.drawImage(this.sprite, this.spriteOffsets[this.spriteState], 0, 20, 20, /* draws to: */ -10, -13, 20, 20);
  },
  
});


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

Game.Levels = {};

window.addEventListener('load', function(evt) {
  window.lifter = new Game();
  window.lifter.start('gamey');
  if (window.location.hash.length > 3) lifter.setLevel(decodeURI(window.location.hash.replace(/^\#/, '')));
});

