// Set dimensions var _height, _width, _margin = 20, _aspect = 16/9, _minw = 640, _minh = _minw/_aspect; if (window.innerWidth > window.innerHeight) { _height = Math.max(_minh, window.innerHeight - 2*_margin); _width = _height*_aspect; } else { _width = Math.max(_minw, window.innerWidth - 2*_margin); _height = _width/_aspect; } // Game constants var constants = { SCREEN_DIM: [_width, _height], SCREEN_CENTER: [_width/2, _height/2], BG_COLOR: '#223322', WORLD_RADIUS: _height/4, WORLD_POS: [_width/2, _height/2], WORLD_CONTOUR: 5, WORLD_CONTOUR_COL: '#003300', ZERO_POTENSIAL_RADIUS: _height*0.6, PLAYER_SPEED: Math.PI/20, // rad/s BARREL_SPEED: Math.PI/10, BARREL_MAX: Math.PI/3, BARREL_MIN: -Math.PI/3, MISSILE_RADIUS: 5, MISSILE_LOAD_SPEED: 100 }; // Game states var state = { LOADING: 1, PLAY: 2, P_CHANGE: 3, PRE_FIRE: 4, FIRE: 5 }; // Event manager var keys_down = {}; addEventListener('keydown', function (e) { keys_down[e.keyCode] = true; }, false); addEventListener('keyup', function (e) { delete keys_down[e.keyCode]; }, false); // Assets. Preload var assets_loaded = 0; function assetLoadCB() { assets_loaded++; } var assets = { space_bg: new Image(), evil_player: new Image(), green_player: new Image(), evil_barrel: new Image(), green_barrel: new Image() }; assets.space_bg.src = 'images/space_bg.jpg'; assets.space_bg.onload = function() {assetLoadCB();}; assets.evil_player.src = 'images/evil.png'; assets.evil_player.onload = function() {assetLoadCB();}; assets.green_player.src = 'images/green.png'; assets.green_player.onload = function() {assetLoadCB();}; assets.evil_barrel.src = 'images/gun.png'; assets.evil_barrel.onload = function() {assetLoadCB();}; assets.green_barrel.src = 'images/gun.png'; assets.green_barrel.onload = function() {assetLoadCB();}; // Timer function Timer() {} Timer.prototype.isset = function() { return this.start !== 'undefined' && this.interval !== 'undefined'; } Timer.prototype.set = function(msec) { this.start = Date.now(); this.interval = msec; } Timer.prototype.finished = function() { return ((Date.now() - this.start) > this.interval) } function Polar(origo, r0, theta0) { this.origo = origo; this.r = r0; this.theta = theta0; } Polar.prototype.euler = function(rp, thetap) { var add_r = (typeof rp === 'undefined') ? 0:rp, add_theta = (typeof thetap === 'undefined') ? 0:thetap; return [this.origo[0] + (this.r + add_r)*Math.cos(this.theta + add_theta), this.origo[1] - (this.r + add_r)*Math.sin(this.theta + add_theta)] } function Missile(ctx, polar0, color) { this.fired = false; this.ctx = ctx; this.polar = polar0; this.polar_v = new Polar(constants.WORLD_POS, 0, 0); this.v0 = 0; this.color = color; } Missile.prototype.fire = function(gun_theta) { this.polar_v.r = Math.cos(gun_theta) * this.v0; this.polar_v.theta = Math.sin(gun_theta) * (Math.PI/180) * this.v0; console.log(this.polar_v.r + ' ' + this.polar_v.theta); this.fired = true; } Missile.prototype.outOfBounds = function() { return (this.euler[0] > constants.SCREEN_DIM[0] || this.euler[0] < 0 || this.euler[1] > constants.SCREEN_DIM[1] || this.euler[1] < 0); } Missile.prototype.checkCollision = function() { return (this.polar.r < constants.WORLD_RADIUS); } function getGravity(r) { var dr = r - (constants.WORLD_RADIUS); return -10000000/(r*r); // Empirical } Missile.prototype.update = function(delta) { this.polar_v.r += getGravity(this.polar.r) * delta; this.polar.r += this.polar_v.r * delta; this.polar.theta += this.polar_v.theta * delta; this.euler = this.polar.euler(); } Missile.prototype.draw = function() { drawFilledCircle(this.ctx, this.euler, constants.MISSILE_RADIUS, 0, 2*Math.PI, this.color, 1, 'black'); } function Player(ctx, is_evil, polar) { this.ctx = ctx; this.polar = polar; this.barrel_theta = 0; // Load images this.image = (is_evil) ? assets.evil_player:assets.green_player; this.barrel_img = (is_evil) ? assets.evil_barrel:assets.green_barrel; //center //console.log(this.image.height); this.polar.theta += this.image.height/(4*Math.PI*constants.WORLD_RADIUS); // simplify, use arcsin. this.polar.r += this.image.width/2; this.euler = this.polar.euler(); } Player.prototype.moveBarrel = function(modifier, delta) { if (!((this.barrel_theta > constants.BARREL_MAX && modifier == 1) || (this.barrel_theta < constants.BARREL_MIN && modifier == -1))) { this.barrel_theta += modifier*constants.BARREL_SPEED*delta; } } Player.prototype.move = function(modifier, delta) { this.polar.theta += modifier*constants.PLAYER_SPEED*delta; this.euler = this.polar.euler(); } Player.prototype.drawBarrel = function() { drawRotatedImage(this.ctx, this.barrel_img, this.polar.euler(this.barrel_img.width + 5), [this.barrel_img.width, this.barrel_img.height], this.polar.theta + this.barrel_theta); } Player.prototype.draw = function() { drawRotatedImage(this.ctx, this.image, this.euler, [this.image.width, this.image.height], this.polar.theta); this.drawBarrel(); } var sector_cols = { 'land': '#8B4513', 'water': '#104E8B', 'ice': '#E0FFFF', 'forest': '#228B22' }; function Sector(ctx, type, theta0, theta) { this.ctx = ctx; this.type = type; this.theta0 = theta0; this.theta = theta; this.health = 0; //[-1, 1] } Sector.prototype.hit = function(h) { if (Math.abs(this.health) === 1 && this.type !== 'ice') { return; //sector is locked } this.health += h; this.health = (this.health < -1) ? -1:this.health; this.health = (this.health > 1) ? 1:this.health; } Sector.prototype.draw = function() { drawFilledCone( this.ctx, constants.WORLD_POS, constants.WORLD_RADIUS, this.theta0, this.theta, sector_cols[this.type] ); if (this.health !== 0) { drawFilledCone( this.ctx, constants.WORLD_POS, constants.WORLD_RADIUS, this.theta0, this.theta, (this.health < 0) ? ('rgba(255, 0, 0, ' + (-this.health) + ')'):('rgba(0, 255, 0, ' + (this.health) + ')') ); } } function Game() { console.log('state LOADING'); this.state = state.LOADING; // Initialize canvases this.bg_canvas = document.getElementById('layer1'); this.g_canvas = document.getElementById('layer2'); this.hud_canvas = document.getElementById('layer3'); this.bg_canvas.width = this.g_canvas.width = this.hud_canvas.width = constants.SCREEN_DIM[0]; this.bg_canvas.height = this.g_canvas.height = this.hud_canvas.height = constants.SCREEN_DIM[1]; this.bg_ctx = this.bg_canvas.getContext('2d'); // BG and world this.draw_bg = true; // Save some resources. Background is pretty static. this.g_ctx = this.g_canvas.getContext('2d'); // Game layer. Always draw this.hud_ctx = this.hud_canvas.getContext('2d'); // Menu and stats this.draw_hud = true; // Also pretty static } function randRange(lower, upper) { return lower + (upper - lower)*Math.random(); } Game.prototype.init = function() { // Instantiate players this.players = [ new Player(this.g_ctx, true, new Polar(constants.WORLD_POS, constants.WORLD_RADIUS, Math.PI/2)), new Player(this.g_ctx, false, new Polar(constants.WORLD_POS, constants.WORLD_RADIUS, Math.PI/2)) ]; this.curr_p = 0; // randomize later this.p_change = new Timer(); // Init sectors var sec_borders = [], slack = 2*Math.PI/20, ice_width = 2*Math.PI/20, sec_width = 2*Math.PI/7; sec_borders.push(0); sec_borders.push(randRange(sec_width - slack, sec_width + slack)); sec_borders.push(randRange(2*sec_width - slack, 2*sec_width + slack)); sec_borders.push(randRange(3*sec_width - slack, 3*sec_width + slack)); sec_borders.push(randRange(4*sec_width - slack, 4*sec_width + slack)); sec_borders.push(randRange(5*sec_width - slack, 5*sec_width + slack)); sec_borders.push(sec_borders[5] + ice_width); sec_borders.push(2*Math.PI); this.sectors = [ new Sector(this.bg_ctx, 'land', sec_borders[0], sec_borders[1]), new Sector(this.bg_ctx, 'water', sec_borders[1], sec_borders[2]), new Sector(this.bg_ctx, 'land', sec_borders[2], sec_borders[3]), new Sector(this.bg_ctx, 'forest', sec_borders[3], sec_borders[4]), new Sector(this.bg_ctx, 'water', sec_borders[4], sec_borders[5]), new Sector(this.bg_ctx, 'ice', sec_borders[5], sec_borders[6]), new Sector(this.bg_ctx, 'forest', sec_borders[6], sec_borders[7]) ]; } Game.prototype.loadAssets = function() { this.g_ctx.save(); this.g_ctx.font = '20px Arial'; this.g_ctx.fillStyle = 'red'; this.g_ctx.fillText('Loading...', constants.SCREEN_CENTER[0] - 40, constants.SCREEN_DIM[1] - 40); this.g_ctx.restore(); if (assets_loaded < Object.keys(assets).length) { return state.LOADING; } else { console.log('state PLAY'); this.init(); return state.PLAY; } } Game.prototype.drawWorld = function() { drawFilledCircle( this.bg_ctx, constants.WORLD_POS, constants.ZERO_POTENSIAL_RADIUS, 0, 2*Math.PI, 'rgba(255, 255, 255, 0.1)' ); drawFilledCircle( this.bg_ctx, constants.WORLD_POS, constants.WORLD_RADIUS, 0, 2*Math.PI, 'white' ); } Game.prototype.drawBGLayer = function() { if (this.draw_bg) { this.bg_ctx.clearRect(0, 0, constants.SCREEN_DIM[0], constants.SCREEN_DIM[1]); var x = y = 0; while (x < constants.SCREEN_DIM[0]) { while (y < constants.SCREEN_DIM[1]) { this.bg_ctx.drawImage(assets.space_bg, x, y); y += assets.space_bg.height; } y = 0 x += assets.space_bg.width; } this.drawWorld(); this.draw_bg = false; } } Game.prototype.drawGLayer = function() { this.g_ctx.clearRect(0, 0, constants.SCREEN_DIM[0], constants.SCREEN_DIM[1]); // Draw sectors for (var i=0; i 2*Math.PI) { _theta -= 2*Math.PI; } while (_theta > this.sectors[i].theta) { i++; } console.log('Hit sector ' + i + ', theta = [' + this.sectors[i].theta0 + ',' + this.sectors[i].theta); console.log('Missile theta ' + _theta); this.sectors[i].hit(randRange(0.2, 0.8) * _modifier); //this.draw_bg = true; } Game.prototype.missileFire = function(delta) { this.missile.update(delta); if (this.missile.checkCollision()) { this.sectorHit(); console.log('state PLAY'); this.state = state.PLAY; } else if (this.missile.outOfBounds()) { console.log('state PLAY'); this.state = state.PLAY; } else { this.missile.draw(); } } Game.prototype.playEventHandler = function(delta) { //space if (keys_down[32]) { this.missile = new Missile( this.g_ctx, new Polar(constants.WORLD_POS, this.players[this.curr_p].polar.r + this.players[this.curr_p].image.width, this.players[this.curr_p].polar.theta), (this.curr_p === 0) ? 'red':'green'); console.log('state PRE_FIRE'); this.state = state.PRE_FIRE; } //enter if (keys_down[13]) { this.p_change.set(1000); console.log('state P_CHANGE'); this.state = state.P_CHANGE; } //left if (keys_down[37]) { this.players[this.curr_p].move(1, delta); } //right if (keys_down[39]) { this.players[this.curr_p].move(-1, delta); } //down if (keys_down[40]) { this.players[this.curr_p].moveBarrel(1, delta); } //up if (keys_down[38]) { this.players[this.curr_p].moveBarrel(-1, delta); } } Game.prototype.mainLoop = function() { var now = Date.now(), delta = (now - this.then)/1000; switch (this.state) { case state.LOADING: this.state = this.loadAssets(); break; case state.PLAY: this.playEventHandler(delta); // more logic this.drawBGLayer(); this.drawGLayer(); break; case state.P_CHANGE: this.drawBGLayer(); this.changePlayer(); break; case state.PRE_FIRE: this.drawBGLayer(); this.drawGLayer(); this.preFire(delta); break; case state.FIRE: this.drawBGLayer(); this.drawGLayer(); this.missileFire(delta); break; default: this.drawBGLayer(); } this.g_ctx.fillText('FPS: ' + parseInt(1/delta), 10, 10); // Show FPS this.then = now; } Game.prototype.run = function() { //game start this.drawWorld(); var self = this; // Alias this. stupid setInterval this.then = Date.now(); setInterval(function() { self.mainLoop(); }, 1); } /* * Helpers * */ function drawColorRect(ctx, pos, dims, col) { ctx.save(); ctx.fillStyle = col; ctx.beginPath(); ctx.rect(pos[0], pos[1], dims[0], dims[1]); ctx.closePath(); ctx.fill(); ctx.restore(); } function drawFilledCone(ctx, pos, r, theta0, theta, fillcol, linewidth, linecol) { ctx.save(); ctx.beginPath(); ctx.arc(pos[0], pos[1], r, -theta, -theta0, false); ctx.lineTo(pos[0], pos[1]); ctx.fillStyle = fillcol; ctx.fill(); if (linewidth !== 'undefined') { ctx.lineWidth = linewidth; ctx.strokeStyle = (linecol === 'undefined') ? 'white':linecol; ctx.stroke(); } ctx.restore(); } function drawFilledCircle(ctx, pos, r, theta0, theta, fillcol, linewidth, linecol) { ctx.save(); ctx.beginPath(); ctx.arc(pos[0], pos[1], r, -theta, -theta0, false); ctx.fillStyle = fillcol; ctx.fill(); if (linewidth !== 'undefined') { ctx.lineWidth = linewidth; ctx.strokeStyle = (linecol === 'undefined') ? 'white':linecol; ctx.stroke(); } ctx.restore(); } function drawRotatedImage(ctx, image, origo, dims, theta) { ctx.save(); ctx.translate(origo[0], origo[1]); ctx.rotate(-theta); ctx.drawImage(image, -dims[0]/2, -dims[1]/2); ctx.restore(); } var game = new Game(); game.run();