pandorafms/pandora_console/include/asteroids/asteroids.js

841 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Asteroids.js
// Copyright (c) 20102023 James Socol <me@jamessocol.com>
// See LICENSE.txt for license terms.
// Game settings
GAME_HEIGHT = 480;
GAME_WIDTH = 640;
FRAME_PERIOD = 60; // 1 frame / x frames/sec
LEVEL_TIMEOUT = 2000; // How long to wait after clearing a level.
// Player settings
ROTATE_SPEED = Math.PI / 10; // How fast do players turn? (radians)
MAX_SPEED = 15; // Maximum player speed
THRUST_ACCEL = 1;
DEATH_TIMEOUT = 2000; // milliseconds
INVINCIBLE_TIMEOUT = 1500; // How long to stay invincible after resurrecting?
PLAYER_LIVES = 3;
POINTS_PER_SHOT = 1; // How many points does a shot cost? (Should be >= 0.)
POINTS_TO_EXTRA_LIFE = 1000; // How many points to get a 1-up?
// Bullet settings
BULLET_SPEED = 20;
MAX_BULLETS = 3;
MAX_BULLET_AGE = 25;
// Asteroid settings
ASTEROID_COUNT = 2; // This + current level = number of asteroids.
ASTEROID_GENERATIONS = 3; // How many times to they split before dying?
ASTEROID_CHILDREN = 2; // How many does each death create?
ASTEROID_SPEED = 3;
ASTEROID_SCORE = 10; // How many points is each one worth?
var Asteroids = function(home) {
// Constructor
// Order matters.
// Set up logging.
this.log_level = Asteroids.LOG_DEBUG;
this.log = Asteroids.logger(this);
// Create the info pane, player, and playfield.
home.innerHTML = "";
this.info = Asteroids.infoPane(this, home);
this.playfield = Asteroids.playfield(this, home);
this.player = Asteroids.player(this);
// Set up the event listeners.
this.keyState = Asteroids.keyState(this);
this.listen = Asteroids.listen(this);
// Useful functions.
this.asteroids = Asteroids.asteroids(this);
this.overlays = Asteroids.overlays(this);
this.highScores = Asteroids.highScores(this);
this.level = Asteroids.level(this);
this.gameOver = Asteroids.gameOver(this);
// Play the game.
Asteroids.play(this);
return this;
};
Asteroids.infoPane = function(game, home) {
var pane = document.createElement("div");
pane.innerHTML = "ASTEROIDS";
var lives = document.createElement("span");
lives.className = "lives";
lives.innerHTML = "LIVES: " + PLAYER_LIVES;
var score = document.createElement("span");
score.className = "score";
score.innerHTML = "SCORE: 0";
var level = document.createElement("span");
level.className = "level";
level.innerHTML = "LEVEL: 1";
pane.appendChild(lives);
pane.appendChild(score);
pane.appendChild(level);
home.appendChild(pane);
return {
setLives: function(game, l) {
lives.innerHTML = "LIVES: " + l;
},
setScore: function(game, s) {
score.innerHTML = "SCORE: " + s;
},
setLevel: function(game, _level) {
level.innerHTML = "LEVEL: " + _level;
},
getPane: function() {
return pane;
}
};
};
Asteroids.playfield = function(game, home) {
var canvas = document.createElement("canvas");
canvas.width = GAME_WIDTH;
canvas.height = GAME_HEIGHT;
home.appendChild(canvas);
return canvas;
};
Asteroids.logger = function(game) {
if (typeof console != "undefined" && typeof console.log != "undefined") {
return {
info: function(msg) {
if (game.log_level <= Asteroids.LOG_INFO) console.log(msg);
},
debug: function(msg) {
if (game.log_level <= Asteroids.LOG_DEBUG) console.log(msg);
},
warning: function(msg) {
if (game.log_level <= Asteroids.LOG_WARNING) console.log(msg);
},
error: function(msg) {
if (game.log_level <= Asteroids.LOG_ERROR) console.log(msg);
},
critical: function(msg) {
if (game.log_level <= Asteroids.LOG_CRITICAL) console.log(msg);
}
};
} else {
return {
info: function(msg) {},
debug: function(msg) {},
warning: function(msg) {},
error: function(msg) {},
critical: function(msg) {}
};
}
};
Asteroids.asteroids = function(game) {
var asteroids = [];
return {
push: function(obj) {
return asteroids.push(obj);
},
pop: function() {
return asteroids.pop();
},
splice: function(i, j) {
return asteroids.splice(i, j);
},
get length() {
return asteroids.length;
},
getIterator: function() {
return asteroids;
},
generationCount: function(_gen) {
var total = 0;
for (var i = 0; i < asteroids.length; i++) {
if (asteroids[i].getGeneration() == _gen) total++;
}
game.log.debug("Found " + total + " asteroids in generation " + _gen);
return total;
}
};
};
/**
* Creates an overlays controller.
*/
Asteroids.overlays = function(game) {
var overlays = [];
return {
draw: function(ctx) {
for (var i = 0; i < overlays.length; i++) {
overlays[i].draw(ctx);
}
},
add: function(obj) {
if (-1 == overlays.indexOf(obj) && typeof obj.draw != "undefined") {
overlays.push(obj);
return true;
}
return false;
},
remove: function(obj) {
var i = overlays.indexOf(obj);
if (-1 != i) {
overlays.splice(i, 1);
return true;
}
return false;
}
};
};
/**
* Creates a player object.
*/
Asteroids.player = function(game) {
// implements IScreenObject
var position = [GAME_WIDTH / 2, GAME_HEIGHT / 2],
velocity = [0, 0],
direction = -Math.PI / 2,
dead = false,
invincible = false,
lastRez = null,
lives = PLAYER_LIVES,
score = 0,
radius = 3,
path = [
[10, 0],
[-5, 5],
[-5, -5],
[10, 0]
];
return {
getPosition: function() {
return position;
},
getVelocity: function() {
return velocity;
},
getSpeed: function() {
return Math.sqrt(Math.pow(velocity[0], 2) + Math.pow(velocity[1], 2));
},
getDirection: function() {
return direction;
},
getRadius: function() {
return radius;
},
getScore: function() {
return score;
},
addScore: function(pts) {
score += pts;
},
lowerScore: function(pts) {
score -= pts;
if (score < 0) {
score = 0;
}
},
getLives: function() {
return lives;
},
rotate: function(rad) {
if (!dead) {
direction += rad;
game.log.info(direction);
}
},
thrust: function(force) {
if (!dead) {
velocity[0] += force * Math.cos(direction);
velocity[1] += force * Math.sin(direction);
if (this.getSpeed() > MAX_SPEED) {
velocity[0] = MAX_SPEED * Math.cos(direction);
velocity[1] = MAX_SPEED * Math.sin(direction);
}
game.log.info(velocity);
}
},
move: function() {
Asteroids.move(position, velocity);
},
draw: function(ctx) {
let color = "#fff";
if (invincible) {
const dt = (new Date() - lastRez) / 200;
const c = Math.floor(Math.cos(dt) * 16).toString(16);
color = `#${c}${c}${c}`;
}
Asteroids.drawPath(ctx, position, direction, 1, path, color);
},
isDead: function() {
return dead;
},
isInvincible: function() {
return invincible;
},
extraLife: function(game) {
game.log.debug("Woo, extra life!");
lives++;
},
die: function(game) {
if (!dead) {
game.log.info("You died!");
dead = true;
invincible = true;
lives--;
position = [GAME_WIDTH / 2, GAME_HEIGHT / 2];
velocity = [0, 0];
direction = -Math.PI / 2;
if (lives > 0) {
setTimeout(
(function(player, _game) {
return function() {
player.resurrect(_game);
};
})(this, game),
DEATH_TIMEOUT
);
} else {
game.gameOver();
}
}
},
resurrect: function(game) {
if (dead) {
dead = false;
invincible = true;
lastRez = new Date();
setTimeout(function() {
invincible = false;
game.log.debug("No longer invincible!");
}, INVINCIBLE_TIMEOUT);
game.log.debug("You ressurrected!");
}
},
fire: function(game) {
if (!dead) {
game.log.debug("You fired!");
var _pos = [position[0], position[1]],
_dir = direction;
this.lowerScore(POINTS_PER_SHOT);
return Asteroids.bullet(game, _pos, _dir);
}
}
};
};
Asteroids.bullet = function(game, _pos, _dir) {
// implements IScreenObject
var position = [_pos[0], _pos[1]],
velocity = [0, 0],
direction = _dir,
age = 0,
radius = 1,
path = [
[0, 0],
[-4, 0]
];
velocity[0] = BULLET_SPEED * Math.cos(_dir);
velocity[1] = BULLET_SPEED * Math.sin(_dir);
return {
getPosition: function() {
return position;
},
getVelocity: function() {
return velocity;
},
getSpeed: function() {
return Math.sqrt(Math.pow(velocity[0], 2) + Math.pow(velocity[1], 2));
},
getRadius: function() {
return radius;
},
getAge: function() {
return age;
},
birthday: function() {
age++;
},
move: function() {
Asteroids.move(position, velocity);
},
draw: function(ctx) {
Asteroids.drawPath(ctx, position, direction, 1, path);
}
};
};
Asteroids.keyState = function(_) {
var state = {
[Asteroids.LEFT]: false,
[Asteroids.UP]: false,
[Asteroids.RIGHT]: false,
[Asteroids.DOWN]: false,
[Asteroids.FIRE]: false
};
return {
on: function(key) {
state[key] = true;
},
off: function(key) {
state[key] = false;
},
getState: function(key) {
if (typeof state[key] != "undefined") return state[key];
return false;
}
};
};
Asteroids.listen = function(game) {
const keyMap = {
ArrowLeft: Asteroids.LEFT,
KeyA: Asteroids.LEFT,
ArrowRight: Asteroids.RIGHT,
KeyD: Asteroids.RIGHT,
ArrowUp: Asteroids.UP,
KeyW: Asteroids.UP,
Space: Asteroids.FIRE
};
window.addEventListener(
"keydown",
function(e) {
const state = keyMap[e.code];
if (state) {
e.preventDefault();
e.stopPropagation();
game.keyState.on(state);
return false;
}
return true;
},
true
);
window.addEventListener(
"keyup",
function(e) {
const state = keyMap[e.code];
if (state) {
e.preventDefault();
e.stopPropagation();
game.keyState.off(state);
return false;
}
return true;
},
true
);
};
Asteroids.asteroid = function(game, _gen) {
// implements IScreenObject
var position = [0, 0],
velocity = [0, 0],
direction = 0,
generation = _gen,
radius = 7,
path = [
[1, 7],
[5, 5],
[7, 1],
[5, -3],
[7, -7],
[3, -9],
[-1, -5],
[-4, -2],
[-8, -1],
[-9, 3],
[-5, 5],
[-1, 3],
[1, 7]
];
return {
getPosition: function() {
return position;
},
setPosition: function(pos) {
position = pos;
},
getVelocity: function() {
return velocity;
},
setVelocity: function(vel) {
velocity = vel;
direction = Math.atan2(vel[1], vel[0]);
},
getSpeed: function() {
return Math.sqrt(Math.pow(velocity[0], 2) + Math.pow(velocity[1], 2));
},
getRadius: function() {
return radius * generation;
},
getGeneration: function() {
return generation;
},
move: function() {
Asteroids.move(position, velocity);
},
draw: function(ctx) {
Asteroids.drawPath(ctx, position, direction, generation, path);
// ctx.setTransform(1, 0, 0, 1, position[0], position[1]);
// ctx.beginPath();
// ctx.arc(0, 0, radius*generation, 0, Math.PI*2, false);
// ctx.stroke();
// ctx.closePath();
}
};
};
Asteroids.collision = function(a, b) {
// if a.getPosition() inside b.getBounds?
var a_pos = a.getPosition(),
b_pos = b.getPosition();
function sq(x) {
return Math.pow(x, 2);
}
var distance = Math.sqrt(sq(a_pos[0] - b_pos[0]) + sq(a_pos[1] - b_pos[1]));
if (distance <= a.getRadius() + b.getRadius()) return true;
return false;
};
Asteroids.level = function(game) {
var level = 0,
speed = ASTEROID_SPEED,
hspeed = ASTEROID_SPEED / 2;
return {
getLevel: function() {
return level;
},
levelUp: function(game) {
level++;
game.log.debug("Congrats! On to level " + level);
while (
game.asteroids.generationCount(ASTEROID_GENERATIONS) <
level + ASTEROID_COUNT
) {
var a = Asteroids.asteroid(game, ASTEROID_GENERATIONS);
a.setPosition([
Math.random() * GAME_WIDTH,
Math.random() * GAME_HEIGHT
]);
a.setVelocity([
Math.random() * speed - hspeed,
Math.random() * speed - hspeed
]);
game.asteroids.push(a);
}
}
};
};
Asteroids.gameOver = function(game) {
return function() {
game.log.debug("Game over!");
if (game.player.getScore() > 0) {
game.highScores.addScore("Player", game.player.getScore());
}
game.overlays.add({
// implements IOverlay
draw: function(ctx) {
ctx.font = "30px System, monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillText("GAME OVER", GAME_WIDTH / 2, GAME_HEIGHT / 2);
var scores = game.highScores.getScores();
ctx.font = "12px System, monospace";
for (var i = 0; i < scores.length; i++) {
ctx.fillText(
scores[i].name + " " + scores[i].score,
GAME_WIDTH / 2,
GAME_HEIGHT / 2 + 20 + 14 * i
);
}
}
});
};
};
Asteroids.highScores = function(game) {
var scores = [];
if ((t = localStorage.getItem("high-scores"))) {
scores = JSON.parse(t);
}
return {
getScores: function() {
return scores;
},
addScore: function(_name, _score) {
scores.push({ name: _name, score: _score });
scores.sort(function(a, b) {
return b.score - a.score;
});
if (scores.length > 10) {
scores.length = 10;
}
game.log.debug("Saving high scores.");
var str = JSON.stringify(scores);
localStorage.setItem("high-scores", str);
}
};
};
Asteroids.drawPath = function(ctx, position, direction, scale, path, color) {
if (!color) {
color = "#fff";
}
ctx.strokeStyle = color;
ctx.setTransform(
Math.cos(direction) * scale,
Math.sin(direction) * scale,
-Math.sin(direction) * scale,
Math.cos(direction) * scale,
position[0],
position[1]
);
ctx.beginPath();
ctx.moveTo(path[0][0], path[0][1]);
for (i = 1; i < path.length; i++) {
ctx.lineTo(path[i][0], path[i][1]);
}
ctx.stroke();
ctx.closePath();
ctx.strokeStyle = "#fff";
};
Asteroids.move = function(position, velocity) {
position[0] += velocity[0];
if (position[0] < 0) position[0] = GAME_WIDTH + position[0];
else if (position[0] > GAME_WIDTH) position[0] -= GAME_WIDTH;
position[1] += velocity[1];
if (position[1] < 0) position[1] = GAME_HEIGHT + position[1];
else if (position[1] > GAME_HEIGHT) position[1] -= GAME_HEIGHT;
};
Asteroids.stars = function() {
var stars = [];
for (var i = 0; i < 50; i++) {
stars.push([Math.random() * GAME_WIDTH, Math.random() * GAME_HEIGHT]);
}
return {
draw: function(ctx) {
var ii = stars.length;
for (var i = 0; i < ii; i++) {
ctx.fillRect(stars[i][0], stars[i][1], 1, 1);
}
}
};
};
Asteroids.play = function(game) {
var ctx = game.playfield.getContext("2d");
ctx.fillStyle = "white";
ctx.strokeStyle = "white";
var speed = ASTEROID_SPEED,
hspeed = ASTEROID_SPEED / 2;
game.level.levelUp(game);
var bullets = [],
last_fire_state = false,
last_asteroid_count = 0;
var extra_lives = 0;
// Add a star field.
game.overlays.add(Asteroids.stars());
game.pulse = setInterval(function() {
var kill_asteroids = [],
new_asteroids = [],
kill_bullets = [];
ctx.save();
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// Be nice and award extra lives first.
var t_extra_lives = game.player.getScore() / POINTS_TO_EXTRA_LIFE;
t_extra_lives = Math.floor(t_extra_lives);
if (t_extra_lives > extra_lives) {
game.player.extraLife(game);
}
extra_lives = t_extra_lives;
if (game.keyState.getState(Asteroids.UP)) {
game.player.thrust(THRUST_ACCEL);
}
if (game.keyState.getState(Asteroids.LEFT)) {
game.player.rotate(-ROTATE_SPEED);
}
if (game.keyState.getState(Asteroids.RIGHT)) {
game.player.rotate(ROTATE_SPEED);
}
var fire_state = game.keyState.getState(Asteroids.FIRE);
if (
fire_state &&
fire_state != last_fire_state &&
bullets.length < MAX_BULLETS
) {
var b = game.player.fire(game);
bullets.push(b);
}
last_fire_state = fire_state;
if (!game.player.isDead()) {
game.player.move();
game.player.draw(ctx);
}
for (var k = 0; k < bullets.length; k++) {
if (!bullets[k]) continue;
if (bullets[k].getAge() > MAX_BULLET_AGE) {
kill_bullets.push(k);
continue;
}
bullets[k].birthday();
bullets[k].move();
bullets[k].draw(ctx);
}
for (var r = kill_bullets.length - 1; r >= 0; r--) {
bullets.splice(r, 1);
}
var asteroids = game.asteroids.getIterator();
for (var i = 0; i < game.asteroids.length; i++) {
var killit = false;
asteroids[i].move();
asteroids[i].draw(ctx);
// Destroy the asteroid
for (var j = 0; j < bullets.length; j++) {
if (!bullets[j]) continue;
if (Asteroids.collision(bullets[j], asteroids[i])) {
game.log.debug("You shot an asteroid!");
// Destroy the bullet.
bullets.splice(j, 1);
killit = true; // JS doesn't have "continue 2;"
continue;
}
}
// Kill the asteroid?
if (killit) {
var _gen = asteroids[i].getGeneration() - 1;
if (_gen > 0) {
// Create children ;)
for (var n = 0; n < ASTEROID_CHILDREN; n++) {
var a = Asteroids.asteroid(game, _gen);
var _pos = [
asteroids[i].getPosition()[0],
asteroids[i].getPosition()[1]
];
a.setPosition(_pos);
a.setVelocity([
Math.random() * speed - hspeed,
Math.random() * speed - hspeed
]);
new_asteroids.push(a);
}
}
game.player.addScore(ASTEROID_SCORE);
kill_asteroids.push(i);
continue;
}
// Kill the player?
if (
!game.player.isDead() &&
!game.player.isInvincible() &&
Asteroids.collision(game.player, asteroids[i])
) {
game.player.die(game);
}
}
kill_asteroids.sort(function(a, b) {
return a - b;
});
for (var m = kill_asteroids.length - 1; m >= 0; m--) {
game.asteroids.splice(kill_asteroids[m], 1);
}
for (var o = 0; o < new_asteroids.length; o++) {
game.asteroids.push(new_asteroids[o]);
}
ctx.restore();
// Do we need to level up?
if (0 == game.asteroids.length && last_asteroid_count != 0) {
setTimeout(function() {
game.level.levelUp(game);
}, LEVEL_TIMEOUT);
}
last_asteroid_count = game.asteroids.length;
// Draw overlays.
game.overlays.draw(ctx);
// Update the info pane.
game.info.setLives(game, game.player.getLives());
game.info.setScore(game, game.player.getScore());
game.info.setLevel(game, game.level.getLevel());
}, FRAME_PERIOD);
};
// Some boring constants.
Asteroids.LOG_ALL = 0;
Asteroids.LOG_INFO = 1;
Asteroids.LOG_DEBUG = 2;
Asteroids.LOG_WARNING = 3;
Asteroids.LOG_ERROR = 4;
Asteroids.LOG_CRITICAL = 5;
Asteroids.LOG_NONE = 6;
Asteroids.LEFT = 37;
Asteroids.UP = 38;
Asteroids.RIGHT = 39;
Asteroids.DOWN = 40;
Asteroids.FIRE = 32;
// Load it up!
window.onload = Asteroids(document.getElementById("asteroids"));