569 lines
14 KiB
JavaScript
Executable File
569 lines
14 KiB
JavaScript
Executable File
// 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<this.sectors.length; i++) {
|
|
this.sectors[i].draw();
|
|
}
|
|
// Draw player
|
|
this.players[this.curr_p].draw();
|
|
}
|
|
|
|
Game.prototype.changePlayer = function() {
|
|
if (this.p_change.finished()) {
|
|
this.curr_p = (this.curr_p === 0) ? 1:0;
|
|
console.log('state PLAY');
|
|
this.state = state.PLAY;
|
|
}
|
|
else {
|
|
var next = (this.curr_p === 0) ? 2:1;
|
|
this.g_ctx.clearRect(0, 0, constants.SCREEN_DIM[0], constants.SCREEN_DIM[1]);
|
|
this.g_ctx.save();
|
|
this.g_ctx.font = '20px Arial';
|
|
this.g_ctx.fillStyle = (next === 1) ? 'red':'green';
|
|
this.g_ctx.fillText(('Player ' + next), constants.SCREEN_CENTER[0] - 40, constants.SCREEN_DIM[1] - 40);
|
|
this.g_ctx.restore();
|
|
}
|
|
}
|
|
|
|
Game.prototype.preFire = function(delta) {
|
|
if (keys_down[32]) {
|
|
this.missile.v0 += constants.MISSILE_LOAD_SPEED * delta;
|
|
}
|
|
else {
|
|
this.missile.fire(this.players[this.curr_p].barrel_theta);
|
|
console.log('state FIRE');
|
|
this.state = state.FIRE;
|
|
}
|
|
}
|
|
|
|
Game.prototype.sectorHit = function() {
|
|
var i = 0,
|
|
_theta = this.missile.polar.theta,
|
|
_modifier = (this.curr_p === 0) ? -1:1;
|
|
|
|
while (_theta < 0) {
|
|
_theta += 2*Math.PI;
|
|
}
|
|
while (_theta > 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();
|
|
|
|
|