| Index: samples/openglui/src/blasteroids.dart
|
| ===================================================================
|
| --- samples/openglui/src/blasteroids.dart (revision 0)
|
| +++ samples/openglui/src/blasteroids.dart (revision 0)
|
| @@ -0,0 +1,3334 @@
|
| +// A Dart port of Kevin Roast's Asteroids game.
|
| +// http://www.kevs3d.co.uk/dev/asteroids
|
| +// Used with permission, including the sound and bitmap assets.
|
| +
|
| +// This should really be multiple files but the embedder doesn't support
|
| +// parts yet. I concatenated the parts in a somewhat random order.
|
| +//
|
| +// Note that Skia seems to have issues with the render compositing modes, so
|
| +// explosions look a bit messy; they aren't transparent where they should be.
|
| +//
|
| +// Currently we use the accelerometer on the phone for direction and thrust.
|
| +// This is hard to control and should probably be changed. The game is also a
|
| +// bit janky on the phone.
|
| +
|
| +library asteroids;
|
| +
|
| +import 'dart:math' as Math;
|
| +import 'gl.dart';
|
| +
|
| +const RAD = Math.PI / 180.0;
|
| +const PI = Math.PI;
|
| +const TWOPI = Math.PI * 2;
|
| +const ONEOPI = 1.0 / Math.PI;
|
| +const PIO2 = Math.PI / 2.0;
|
| +const PIO4 = Math.PI / 4.0;
|
| +const PIO8 = Math.PI / 8.0;
|
| +const PIO16 = Math.PI / 16.0;
|
| +const PIO32 = Math.PI / 32.0;
|
| +
|
| +var _rnd = new Math.Random();
|
| +double random() => _rnd.nextDouble();
|
| +int randomInt(int min, int max) => min + _rnd.nextInt(max - min + 1);
|
| +
|
| +class Key {
|
| + static const SHIFT = 16;
|
| + static const CTRL = 17;
|
| + static const ESC = 27;
|
| + static const RIGHT = 39;
|
| + static const UP = 38;
|
| + static const LEFT = 37;
|
| + static const DOWN = 40;
|
| + static const SPACE = 32;
|
| + static const A = 65;
|
| + static const E = 69;
|
| + static const G = 71;
|
| + static const L = 76;
|
| + static const P = 80;
|
| + static const R = 82;
|
| + static const S = 83;
|
| + static const Z = 90;
|
| +}
|
| +
|
| +// Globals
|
| +var Debug = {
|
| + 'enabled': false,
|
| + 'invincible': false,
|
| + 'collisionRadius': false,
|
| + 'fps': true
|
| +};
|
| +
|
| +var glowEffectOn = true;
|
| +const GLOWSHADOWBLUR = 8;
|
| +const SCOREDBKEY = "asteroids-score-1.1";
|
| +
|
| +var _asteroidImgs = [];
|
| +var _shieldImg = new ImageElement();
|
| +var _backgroundImg = new ImageElement();
|
| +var _playerImg = new ImageElement();
|
| +var _enemyshipImg = new ImageElement();
|
| +var soundManager;
|
| +
|
| +/** Asteroids color constants */
|
| +class Colors {
|
| + static const PARTICLE = "rgb(255,125,50)";
|
| + static const ENEMY_SHIP = "rgb(200,200,250)";
|
| + static const ENEMY_SHIP_DARK = "rgb(150,150,200)";
|
| + static const GREEN_LASER = "rgb(120,255,120)";
|
| + static const GREEN_LASER_DARK = "rgb(50,255,50)";
|
| + static const GREEN_LASERX2 = "rgb(120,255,150)";
|
| + static const GREEN_LASERX2_DARK = "rgb(50,255,75)";
|
| + static const PLAYER_BOMB = "rgb(155,255,155)";
|
| + static const PLAYER_THRUST = "rgb(25,125,255)";
|
| + static const PLAYER_SHIELD = "rgb(100,100,255)";
|
| +}
|
| +
|
| +/**
|
| + * Actor base class.
|
| + *
|
| + * Game actors have a position in the game world and a current vector to
|
| + * indicate direction and speed of travel per frame. They each support the
|
| + * onUpdate() and onRender() event methods, finally an actor has an expired()
|
| + * method which should return true when the actor object should be removed
|
| + * from play.
|
| + */
|
| +class Actor {
|
| + Vector position, velocity;
|
| +
|
| + Actor(this.position, this.velocity);
|
| +
|
| + /**
|
| + * Actor game loop update event method. Called for each actor
|
| + * at the start of each game loop cycle.
|
| + */
|
| + onUpdate(Scene scene) {}
|
| +
|
| + /**
|
| + * Actor rendering event method. Called for each actor to
|
| + * render for each frame.
|
| + */
|
| + void onRender(CanvasRenderingContext2D ctx) {}
|
| +
|
| + /**
|
| + * Actor expiration test; return true if expired and to be removed
|
| + * from the actor list, false if still in play.
|
| + */
|
| + bool expired() => false;
|
| +
|
| + get frameMultiplier => GameHandler.frameMultiplier;
|
| + get frameStart => GameHandler.frameStart;
|
| + get canvas_height => GameHandler.height;
|
| + get canvas_width => GameHandler.width;
|
| +}
|
| +
|
| +// Short-lived actors (like particles and munitions). These have a
|
| +// start time and lifespan, and fade out after a period.
|
| +
|
| +class ShortLivedActor extends Actor {
|
| + int lifespan;
|
| + int start;
|
| +
|
| + ShortLivedActor(Vector position, Vector velocity,
|
| + this.lifespan)
|
| + : super(position, velocity),
|
| + this.start = GameHandler.frameStart;
|
| +
|
| + bool expired() => (frameStart - start > lifespan);
|
| +
|
| + /**
|
| + * Helper to return a value multiplied by the ratio of the remaining lifespan
|
| + */
|
| + double fadeValue(double val, int offset) {
|
| + var rem = lifespan - (frameStart - start),
|
| + result = val;
|
| + if (rem < offset) {
|
| + result = (val / offset) * rem;
|
| + result = Math.max(0.0, Math.min(result, val));
|
| + }
|
| + return result;
|
| + }
|
| +}
|
| +
|
| +class AttractorScene extends Scene {
|
| + AsteroidsMain game;
|
| +
|
| + AttractorScene(this.game)
|
| + : super(false, null) {
|
| + }
|
| +
|
| + bool start = false;
|
| + bool imagesLoaded = false;
|
| + double sine = 0.0;
|
| + double mult = 0.0;
|
| + double multIncrement = 0.0;
|
| + List actors = null;
|
| + const SCENE_LENGTH = 400;
|
| + const SCENE_FADE = 75;
|
| + List sceneRenderers = null;
|
| + int currentSceneRenderer = 0;
|
| + int currentSceneFrame = 0;
|
| +
|
| + bool isComplete() => start;
|
| +
|
| + void onInitScene() {
|
| + start = false;
|
| + mult = 512.0;
|
| + multIncrement = 0.5;
|
| + currentSceneRenderer = 0;
|
| + currentSceneFrame = 0;
|
| +
|
| + // scene renderers
|
| + // display welcome text, info text and high scores
|
| + sceneRenderers = [
|
| + sceneRendererWelcome,
|
| + sceneRendererInfo,
|
| + sceneRendererScores ];
|
| +
|
| + // randomly generate some background asteroids for attractor scene
|
| + actors = [];
|
| + for (var i = 0; i < 8; i++) {
|
| + var pos = new Vector(random() * GameHandler.width.toDouble(),
|
| + random() * GameHandler.height.toDouble());
|
| + var vec = new Vector(((random() * 2.0) - 1.0), ((random() * 2.0) - 1.0));
|
| + actors.add(new Asteroid(pos, vec, randomInt(3, 4)));
|
| + }
|
| +
|
| + game.score = 0;
|
| + game.lives = 3;
|
| + }
|
| +
|
| + void onRenderScene(CanvasRenderingContext2D ctx) {
|
| + if (imagesLoaded) {
|
| + // Draw the background asteroids.
|
| + for (var i = 0; i < actors.length; i++) {
|
| + var actor = actors[i];
|
| + actor.onUpdate(this);
|
| + game.updateActorPosition(actor);
|
| + actor.onRender(ctx);
|
| + }
|
| +
|
| + // Handle cycling through scenes.
|
| + if (++currentSceneFrame == SCENE_LENGTH) { // Move to next scene.
|
| + if (++currentSceneRenderer == sceneRenderers.length) {
|
| + currentSceneRenderer = 0; // Wrap to first scene.
|
| + }
|
| + currentSceneFrame = 0;
|
| + }
|
| +
|
| + ctx.save();
|
| +
|
| + // fade in/out
|
| + if (currentSceneFrame < SCENE_FADE) {
|
| + // fading in
|
| + ctx.globalAlpha = 1 - ((SCENE_FADE - currentSceneFrame) / SCENE_FADE);
|
| + } else if (currentSceneFrame >= SCENE_LENGTH - SCENE_FADE) {
|
| + // fading out
|
| + ctx.globalAlpha = ((SCENE_LENGTH - currentSceneFrame) / SCENE_FADE);
|
| + } else {
|
| + ctx.globalAlpha = 1.0;
|
| + }
|
| +
|
| + sceneRenderers[currentSceneRenderer](ctx);
|
| +
|
| + ctx.restore();
|
| +
|
| + sineText(ctx, "BLASTEROIDS",
|
| + GameHandler.width ~/ 2 - 130, GameHandler.height ~/ 2 - 64);
|
| + } else {
|
| + centerFillText(ctx, "Loading...",
|
| + "18pt Courier New", GameHandler.height ~/ 2, "white");
|
| + }
|
| + }
|
| +
|
| + void sceneRendererWelcome(CanvasRenderingContext2D ctx) {
|
| + ctx.fillStyle = ctx.strokeStyle = "white";
|
| + centerFillText(ctx, "Press SPACE or click to start", "18pt Courier New",
|
| + GameHandler.height ~/ 2);
|
| + fillText(ctx, "based on Javascript game by Kevin Roast",
|
| + "10pt Courier New", 16, 624);
|
| + }
|
| +
|
| + void sceneRendererInfo(CanvasRenderingContext2D ctx) {
|
| + ctx.fillStyle = ctx.strokeStyle = "white";
|
| + fillText(ctx, "How to play...", "14pt Courier New", 40, 320);
|
| + fillText(ctx, "Arrow keys or tilt to rotate, thrust, shield. "
|
| + "SPACE or touch to fire.",
|
| + "14pt Courier New", 40, 350);
|
| + fillText(ctx, "Pickup the glowing power-ups to enhance your ship.",
|
| + "14pt Courier New", 40, 370);
|
| + fillText(ctx, "Watch out for enemy saucers!", "14pt Courier New", 40, 390);
|
| + }
|
| +
|
| + void sceneRendererScores(CanvasRenderingContext2D ctx) {
|
| + ctx.fillStyle = ctx.strokeStyle = "white";
|
| + centerFillText(ctx, "High Score", "18pt Courier New", 320);
|
| + var sscore = this.game.highscore.toString();
|
| + // pad with zeros
|
| + for (var i=0, j=8-sscore.length; i<j; i++) {
|
| + sscore = "0$sscore";
|
| + }
|
| + centerFillText(ctx, sscore, "18pt Courier New", 350);
|
| + }
|
| +
|
| + /** Callback from image preloader when all images are ready */
|
| + void ready() {
|
| + imagesLoaded = true;
|
| + }
|
| +
|
| + /**
|
| + * Render the a text string in a pulsing x-sine y-cos wave pattern
|
| + * The multiplier for the sinewave is modulated over time
|
| + */
|
| + void sineText(CanvasRenderingContext2D ctx, String txt, int xpos, int ypos) {
|
| + mult += multIncrement;
|
| + if (mult > 1024.0) {
|
| + multIncrement = -multIncrement;
|
| + } else if (this.mult < 128.0) {
|
| + multIncrement = -multIncrement;
|
| + }
|
| + var offset = sine;
|
| + for (var i = 0; i < txt.length; i++) {
|
| + var y = ypos + ((Math.sin(offset) * RAD) * mult).toInt();
|
| + var x = xpos + ((Math.cos(offset++) * RAD) * (mult * 0.5)).toInt();
|
| + fillText(ctx, txt[i], "36pt Courier New", x + i * 30, y, "white");
|
| + }
|
| + sine += 0.075;
|
| + }
|
| +
|
| + bool onKeyDownHandler(int keyCode) {
|
| + log("In onKeyDownHandler, AttractorScene");
|
| + switch (keyCode) {
|
| + case Key.SPACE:
|
| + if (imagesLoaded) {
|
| + start = true;
|
| + }
|
| + return true;
|
| + case Key.ESC:
|
| + GameHandler.togglePause();
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + bool onMouseDownHandler(e) {
|
| + if (imagesLoaded) {
|
| + start = true;
|
| + }
|
| + return true;
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * An actor representing a transient effect in the game world. An effect is
|
| + * nothing more than a special graphic that does not play any direct part in
|
| + * the game and does not interact with any other objects. It automatically
|
| + * expires after a set lifespan, generally the rendering of the effect is
|
| + * based on the remaining lifespan.
|
| + */
|
| +class EffectActor extends Actor {
|
| + int lifespan; // in msec.
|
| + int effectStart; // start time
|
| +
|
| + EffectActor(Vector position , Vector velocity, [this.lifespan = 0])
|
| + : super(position, velocity) {
|
| + effectStart = frameStart;
|
| + }
|
| +
|
| + bool expired() => (frameStart - effectStart > lifespan);
|
| +
|
| + /**
|
| + * Helper for an effect to return the value multiplied by the ratio of the
|
| + * remaining lifespan of the effect.
|
| + */
|
| + double effectValue(double val) {
|
| + var result = val - (val * (frameStart - effectStart)) / lifespan;
|
| + return Math.max(0.0, Math.min(val, result));
|
| + }
|
| +}
|
| +
|
| +/** Text indicator effect actor class. */
|
| +class TextIndicator extends EffectActor {
|
| + int fadeLength;
|
| + int textSize;
|
| + String msg;
|
| + String color;
|
| +
|
| + TextIndicator(Vector position, Vector velocity, this.msg,
|
| + [this.textSize = 12, this.color = "white",
|
| + int fl = 500]) :
|
| + super(position, velocity, fl), fadeLength = fl;
|
| +
|
| + const DEFAULT_FADE_LENGTH = 500;
|
| +
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + // Fade out alpha.
|
| + ctx.save();
|
| + ctx.globalAlpha = effectValue(1.0);
|
| + fillText(ctx, msg, "${textSize}pt Courier New",
|
| + position.x, position.y, color);
|
| + ctx.restore();
|
| + }
|
| +}
|
| +
|
| +/** Score indicator effect actor class. */
|
| +class ScoreIndicator extends TextIndicator {
|
| + ScoreIndicator(Vector position, Vector velocity, int score,
|
| + [int textSize = 12, String prefix = '', String color = "white",
|
| + int fadeLength = 500]) :
|
| + super(position, velocity, '${prefix.length > 0 ? "$prefix " : ""}${score}',
|
| + textSize, color, fadeLength);
|
| +}
|
| +
|
| +/** Power up collectable. */
|
| +class PowerUp extends EffectActor {
|
| + PowerUp(Vector position, Vector velocity)
|
| + : super(position, velocity);
|
| +
|
| + const RADIUS = 8;
|
| + int pulse = 128;
|
| + int pulseinc = 5;
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + ctx.save();
|
| + ctx.globalAlpha = 0.75;
|
| + var col = "rgb(255,${pulse.toString()},0)";
|
| + ctx.fillStyle = col;
|
| + ctx.strokeStyle = "rgb(255,255,128)";
|
| + ctx.beginPath();
|
| + ctx.arc(position.x, position.y, RADIUS, 0, TWOPI, true);
|
| + ctx.closePath();
|
| + ctx.fill();
|
| + ctx.stroke();
|
| + ctx.restore();
|
| + pulse += pulseinc;
|
| + if (pulse > 255){
|
| + pulse = 256 - pulseinc;
|
| + pulseinc =- pulseinc;
|
| + } else if (pulse < 0) {
|
| + pulse = 0 - pulseinc;
|
| + pulseinc =- pulseinc;
|
| + }
|
| + }
|
| +
|
| + get radius => RADIUS;
|
| +
|
| + void collected(AsteroidsMain game, Player player, GameScene scene) {
|
| + // Randomly select a powerup to apply.
|
| + var message = null;
|
| + var n, m, enemy, pos;
|
| + switch (randomInt(0, 9)) {
|
| + case 0:
|
| + case 1:
|
| + message = "Energy Boost!";
|
| + player.energy += player.ENERGY_INIT / 2;
|
| + if (player.energy > player.ENERGY_INIT) {
|
| + player.energy = player.ENERGY_INIT;
|
| + }
|
| + break;
|
| +
|
| + case 2:
|
| + message = "Fire When Shielded!";
|
| + player.fireWhenShield = true;
|
| + break;
|
| +
|
| + case 3:
|
| + message = "Extra Life!";
|
| + game.lives++;
|
| + break;
|
| +
|
| + case 4:
|
| + message = "Slow Down Asteroids!";
|
| + m = scene.enemies.length;
|
| + for (n = 0; n < m; n++) {
|
| + enemy = scene.enemies[n];
|
| + if (enemy is Asteroid) {
|
| + enemy.velocity.scale(0.66);
|
| + }
|
| + }
|
| + break;
|
| +
|
| + case 5:
|
| + message = "Smart Bomb!";
|
| +
|
| + var effectRad = 96;
|
| +
|
| + // Add a BIG explosion actor at the smart bomb weapon position
|
| + // and vector.
|
| + var boom = new Explosion(position.clone(),
|
| + velocity.nscale(0.5), effectRad / 8);
|
| + scene.effects.add(boom);
|
| +
|
| + // Test circle intersection with each enemy actor.
|
| + // We check the enemy list length each iteration to catch baby asteroids
|
| + // this is a fully fledged smart bomb after all!
|
| + pos = position;
|
| + for (n = 0; n < scene.enemies.length; n++) {
|
| + enemy = scene.enemies[n];
|
| +
|
| + // Test the distance against the two radius combined.
|
| + if (pos.distance(enemy.position) <= effectRad + enemy.radius) {
|
| + // Intersection detected!
|
| + enemy.hit(-1);
|
| + scene.generatePowerUp(enemy);
|
| + scene.destroyEnemy(enemy, velocity, true);
|
| + }
|
| + }
|
| + break;
|
| +
|
| + case 6:
|
| + message = "Twin Cannons!";
|
| + player.primaryWeapons["main"] = new TwinCannonsWeapon(player);
|
| + break;
|
| +
|
| + case 7:
|
| + message = "Spray Cannons!";
|
| + player.primaryWeapons["main"] = new VSprayCannonsWeapon(player);
|
| + break;
|
| +
|
| + case 8:
|
| + message = "Rear Gun!";
|
| + player.primaryWeapons["rear"] = new RearGunWeapon(player);
|
| + break;
|
| +
|
| + case 9:
|
| + message = "Side Guns!";
|
| + player.primaryWeapons["side"] = new SideGunWeapon(player);
|
| + break;
|
| + }
|
| +
|
| + if (message != null) {
|
| + // Generate a effect indicator at the destroyed enemy position.
|
| + var vec = new Vector(0.0, -1.5);
|
| + var effect = new TextIndicator(
|
| + new Vector(position.x, position.y - RADIUS), vec,
|
| + message, null, null, 700);
|
| + scene.effects.add(effect);
|
| + }
|
| + }
|
| +}
|
| +/**
|
| + * This is the common base class of actors that can be hit and destroyed by
|
| + * player bullets. It supports a hit() method which should return true when
|
| + * the enemy object should be removed from play.
|
| + */
|
| +class EnemyActor extends SpriteActor {
|
| + EnemyActor(Vector position, Vector velocity, this.size)
|
| + : super(position, velocity);
|
| +
|
| + bool alive = true;
|
| +
|
| + /** Size - values from 1-4 are valid for asteroids, 0-1 for ships. */
|
| + int size;
|
| +
|
| + bool expired() => !alive;
|
| +
|
| + bool hit(num force) {
|
| + alive = false;
|
| + return true;
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Asteroid actor class.
|
| + */
|
| +class Asteroid extends EnemyActor {
|
| + Asteroid(Vector position, Vector velocity, int size, [this.type])
|
| + : super(position, velocity, size) {
|
| + health = size;
|
| +
|
| + // Randomly select an asteroid image bitmap.
|
| + if (type == null) {
|
| + type = randomInt(1, 4);
|
| + }
|
| + animImage = _asteroidImgs[type-1];
|
| +
|
| + // Rrandomly setup animation speed and direction.
|
| + animForward = (random() < 0.5);
|
| + animSpeed = 0.3 + random() * 0.5;
|
| + animLength = ANIMATION_LENGTH;
|
| + rotation = randomInt(0, 180);
|
| + rotationSpeed = (random() - 0.5) / 30;
|
| + }
|
| +
|
| + const ANIMATION_LENGTH = 180;
|
| +
|
| + /** Asteroid graphic type i.e. which bitmap it is drawn from. */
|
| + int type;
|
| +
|
| + /** Asteroid health before it's destroyed. */
|
| + num health = 0;
|
| +
|
| + /** Retro graphics mode rotation orientation and speed. */
|
| + int rotation = 0;
|
| + double rotationSpeed = 0.0;
|
| +
|
| + /** Asteroid rendering method. */
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + var rad = size * 8;
|
| + ctx.save();
|
| + // Render asteroid graphic bitmap. The bitmap is rendered slightly large
|
| + // than the radius as the raytraced asteroid graphics do not quite touch
|
| + // the edges of the 64x64 sprite - this improves perceived collision
|
| + // detection.
|
| + renderSprite(ctx, position.x - rad - 2, position.y - rad - 2, (rad * 2)+4);
|
| + ctx.restore();
|
| + }
|
| +
|
| + get radius => size * 8;
|
| +
|
| + bool hit(num force) {
|
| + if (force != -1) {
|
| + health -= force;
|
| + } else {
|
| + // instant kill
|
| + health = 0;
|
| + }
|
| + return !(alive = (health > 0));
|
| + }
|
| +}
|
| +
|
| +/** Enemy Ship actor class. */
|
| +class EnemyShip extends EnemyActor {
|
| +
|
| + get radius => _radius;
|
| +
|
| + EnemyShip(GameScene scene, int size)
|
| + : super(null, null, size) {
|
| + // Small ship, alter settings slightly.
|
| + if (size == 1) {
|
| + BULLET_RECHARGE_MS = 1300;
|
| + _radius = 8;
|
| + } else {
|
| + _radius = 16;
|
| + }
|
| +
|
| + // Randomly setup enemy initial position and vector
|
| + // ensure the enemy starts in the opposite quadrant to the player.
|
| + var p, v;
|
| + if (scene.player.position.x < canvas_width / 2) {
|
| + // Player on left of the screen.
|
| + if (scene.player.position.y < canvas_height / 2) {
|
| + // Player in top left of the screen.
|
| + position = new Vector(canvas_width-48, canvas_height-48);
|
| + } else {
|
| + // Player in bottom left of the screen.
|
| + position = new Vector(canvas_width-48, 48);
|
| + }
|
| + velocity = new Vector(-(random() + 0.25 + size * 0.75),
|
| + random() + 0.25 + size * 0.75);
|
| + } else {
|
| + // Player on right of the screen.
|
| + if (scene.player.position.y < canvas_height / 2) {
|
| + // Player in top right of the screen.
|
| + position = new Vector(0, canvas_height-48);
|
| + } else {
|
| + // Player in bottom right of the screen.
|
| + position = new Vector(0, 48);
|
| + }
|
| + velocity = new Vector(random() + 0.25 + size * 0.75,
|
| + random() + 0.25 + size * 0.75);
|
| + }
|
| +
|
| + // Setup SpriteActor values.
|
| + animImage = _enemyshipImg;
|
| + animLength = SHIP_ANIM_LENGTH;
|
| + }
|
| +
|
| + const SHIP_ANIM_LENGTH = 90;
|
| + int _radius;
|
| + int BULLET_RECHARGE_MS = 1800;
|
| +
|
| +
|
| + /** True if ship alive, false if ready for expiration. */
|
| + bool alive = true;
|
| +
|
| + /** Bullet fire recharging counter. */
|
| + int bulletRecharge = 0;
|
| +
|
| + void onUpdate(GameScene scene) {
|
| + // change enemy direction randomly
|
| + if (size == 0) {
|
| + if (random() < 0.01) {
|
| + velocity.y = -(velocity.y + (0.25 - (random()/2)));
|
| + }
|
| + } else {
|
| + if (random() < 0.02) {
|
| + velocity.y = -(velocity.y + (0.5 - random()));
|
| + }
|
| + }
|
| +
|
| + // regular fire a bullet at the player
|
| + if (frameStart - bulletRecharge >
|
| + BULLET_RECHARGE_MS && scene.player.alive) {
|
| + // ok, update last fired time and we can now generate a bullet
|
| + bulletRecharge = frameStart;
|
| +
|
| + // generate a vector pointed at the player
|
| + // by calculating a vector between the player and enemy positions
|
| + var v = scene.player.position.clone().sub(position);
|
| + // scale resulting vector down to bullet vector size
|
| + var scale = (size == 0 ? 3.0 : 3.5) / v.length();
|
| + v.x *= scale;
|
| + v.y *= scale;
|
| + // slightly randomize the direction (big ship is less accurate also)
|
| + v.x += (size == 0 ? (random() * 2.0 - 1.0) : (random() - 0.5));
|
| + v.y += (size == 0 ? (random() * 2.0 - 1.0) : (random() - 0.5));
|
| + // - could add the enemy motion vector for correct momentum
|
| + // - but this leads to slow bullets firing back from dir of travel
|
| + // - so pretend that enemies are clever enough to account for this...
|
| + //v.add(this.vector);
|
| +
|
| + var bullet = new EnemyBullet(position.clone(), v);
|
| + scene.enemyBullets.add(bullet);
|
| + //soundManager.play('enemy_bomb');
|
| + }
|
| + }
|
| +
|
| + /** Enemy rendering method. */
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + // render enemy graphic bitmap
|
| + var rad = radius + 2;
|
| + renderSprite(ctx, position.x - rad, position.y - rad, rad * 2);
|
| + }
|
| +
|
| + /** Enemy hit by a bullet; return true if destroyed, false otherwise. */
|
| + bool hit(num force) {
|
| + alive = false;
|
| + return true;
|
| + }
|
| +
|
| + bool expired() {
|
| + return !alive;
|
| + }
|
| +}
|
| +
|
| +class GameCompleted extends Scene {
|
| + AsteroidsMain game;
|
| + var player;
|
| +
|
| + GameCompleted(this.game)
|
| + : super(false) {
|
| + interval = new Interval("CONGRATULATIONS!", intervalRenderer);
|
| + player = game.player;
|
| + }
|
| +
|
| + bool isComplete() => true;
|
| +
|
| + void intervalRenderer(Interval interval, CanvasRenderingContext2D ctx) {
|
| + if (interval.framecounter++ == 0) {
|
| + if (game.score == game.highscore) {
|
| + // save new high score to HTML5 local storage
|
| + if (window.localStorage) {
|
| + window.localStorage[SCOREDBKEY] = game.score;
|
| + }
|
| + }
|
| + }
|
| + if (interval.framecounter < 1000) {
|
| + fillText(ctx, interval.label, "18pt Courier New",
|
| + GameHandler.width ~/ 2 - 96, GameHandler.height ~/ 2 - 32, "white");
|
| + fillText(ctx, "Score: ${game.score}", "14pt Courier New",
|
| + GameHandler.width ~/ 2 - 64, GameHandler.height ~/ 2, "white");
|
| + if (game.score == game.highscore) {
|
| + fillText(ctx, "New High Score!", "14pt Courier New",
|
| + GameHandler.width ~/ 2 - 64,
|
| + GameHandler.height ~/ 2 + 24, "white");
|
| + }
|
| + } else {
|
| + interval.complete = true;
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Game Handler.
|
| + *
|
| + * Singleton instance responsible for managing the main game loop and
|
| + * maintaining a few global references such as the canvas and frame counters.
|
| + */
|
| +class GameHandler {
|
| + /**
|
| + * The single Game.Main derived instance
|
| + */
|
| + static GameMain game = null;
|
| +
|
| + static bool paused = false;
|
| + static CanvasElement canvas = null;
|
| + static int width = 0;
|
| + static int height = 0;
|
| + static int frameCount = 0;
|
| +
|
| + /** Frame multiplier - i.e. against the ideal fps. */
|
| + static double frameMultiplier = 1.0;
|
| +
|
| + /** Last frame start time in ms. */
|
| + static int frameStart = 0;
|
| +
|
| + /** Debugging output. */
|
| + static int maxfps = 0;
|
| +
|
| + /** Ideal FPS constant. */
|
| + static const FPSMS = 1000 / 60;
|
| +
|
| + static Prerenderer bitmaps;
|
| +
|
| + /** Init function called once by your window.onload handler. */
|
| + static void init(c) {
|
| + canvas = c;
|
| + width = canvas.width;
|
| + height = canvas.height;
|
| + log("Init GameMain($c,$width,$height)");
|
| + }
|
| +
|
| + /**
|
| + * Game start method - begins the main game loop.
|
| + * Pass in the object that represent the game to execute.
|
| + */
|
| + static void start(GameMain g) {
|
| + game = g;
|
| + frameStart = new DateTime.now().millisecondsSinceEpoch;
|
| + log("Doing first frame");
|
| + game.frame();
|
| + }
|
| +
|
| + /** Called each frame by the main game loop unless paused. */
|
| + static void doFrame(_) {
|
| + log("Doing next frame");
|
| + game.frame();
|
| + }
|
| +
|
| + static void togglePause() {
|
| + if (paused) {
|
| + paused = false;
|
| + frameStart = new DateTime.now().millisecondsSinceEpoch;
|
| + game.frame();
|
| + } else {
|
| + paused = true;
|
| + }
|
| + }
|
| +
|
| + static bool onAccelerometer(double x, double y, double z) {
|
| + return game == null ? true : game.onAccelerometer(x, y, z);
|
| + }
|
| +}
|
| +
|
| +bool onAccelerometer(double x, double y, double z) {
|
| + return GameHandler.onAccelerometer(x, y, z);
|
| +}
|
| +
|
| +/** Game main loop class. */
|
| +class GameMain {
|
| +
|
| + GameMain() {
|
| + var me = this;
|
| +
|
| + document.onKeyDown.listen((KeyboardEvent event) {
|
| + var keyCode = event.keyCode;
|
| +
|
| + log("In document.onKeyDown($keyCode)");
|
| + if (me.sceneIndex != -1) {
|
| + if (me.scenes[me.sceneIndex].onKeyDownHandler(keyCode) != null) {
|
| + // if the key is handled, prevent any further events
|
| + if (event != null) {
|
| + event.preventDefault();
|
| + event.stopPropagation();
|
| + }
|
| + }
|
| + }
|
| + });
|
| +
|
| + document.onKeyUp.listen((KeyboardEvent event) {
|
| + var keyCode = event.keyCode;
|
| + if (me.sceneIndex != -1) {
|
| + if (me.scenes[me.sceneIndex].onKeyUpHandler(keyCode) != null) {
|
| + // if the key is handled, prevent any further events
|
| + if (event != null) {
|
| + event.preventDefault();
|
| + event.stopPropagation();
|
| + }
|
| + }
|
| + }
|
| + });
|
| +
|
| + document.onMouseDown.listen((MouseEvent event) {
|
| + if (me.sceneIndex != -1) {
|
| + if (me.scenes[me.sceneIndex].onMouseDownHandler(event) != null) {
|
| + // if the event is handled, prevent any further events
|
| + if (event != null) {
|
| + event.preventDefault();
|
| + event.stopPropagation();
|
| + }
|
| + }
|
| + }
|
| + });
|
| +
|
| + document.onMouseUp.listen((MouseEvent event) {
|
| + if (me.sceneIndex != -1) {
|
| + if (me.scenes[me.sceneIndex].onMouseUpHandler(event) != null) {
|
| + // if the event is handled, prevent any further events
|
| + if (event != null) {
|
| + event.preventDefault();
|
| + event.stopPropagation();
|
| + }
|
| + }
|
| + }
|
| + });
|
| +
|
| + }
|
| +
|
| + List scenes = [];
|
| + Scene startScene = null;
|
| + Scene endScene = null;
|
| + Scene currentScene = null;
|
| + int sceneIndex = -1;
|
| + var interval = null;
|
| + int totalFrames = 0;
|
| +
|
| + bool onAccelerometer(double x, double y, double z) {
|
| + if (currentScene != null) {
|
| + return currentScene.onAccelerometer(x, y, z);
|
| + }
|
| + return true;
|
| + }
|
| + /**
|
| + * Game frame execute method - called by anim handler timeout
|
| + */
|
| + void frame() {
|
| + var frameStart = new DateTime.now().millisecondsSinceEpoch;
|
| +
|
| + // Calculate scene transition and current scene.
|
| + if (currentScene == null) {
|
| + // Set to scene zero (game init).
|
| + currentScene = scenes[sceneIndex = 0];
|
| + currentScene.onInitScene();
|
| + } else if (isGameOver()) {
|
| + sceneIndex = -1;
|
| + currentScene = endScene;
|
| + currentScene.onInitScene();
|
| + }
|
| +
|
| + if ((currentScene.interval == null ||
|
| + currentScene.interval.complete) && currentScene.isComplete()) {
|
| + if (++sceneIndex >= scenes.length){
|
| + sceneIndex = 0;
|
| + }
|
| + currentScene = scenes[sceneIndex];
|
| + currentScene.onInitScene();
|
| + }
|
| +
|
| + var ctx = GameHandler.canvas.getContext('2d');
|
| +
|
| + // Rrender the game and current scene.
|
| + ctx.save();
|
| + if (currentScene.interval == null || currentScene.interval.complete) {
|
| + currentScene.onBeforeRenderScene();
|
| + onRenderGame(ctx);
|
| + currentScene.onRenderScene(ctx);
|
| + } else {
|
| + onRenderGame(ctx);
|
| + currentScene.interval.intervalRenderer(currentScene.interval, ctx);
|
| + }
|
| + ctx.restore();
|
| +
|
| + GameHandler.frameCount++;
|
| +
|
| + // Calculate frame total time interval and frame multiplier required
|
| + // for smooth animation.
|
| +
|
| + // Time since last frame.
|
| + var frameInterval = frameStart - GameHandler.frameStart;
|
| + if (frameInterval == 0) frameInterval = 1;
|
| + if (GameHandler.frameCount % 16 == 0) { // Update fps every 16 frames
|
| + GameHandler.maxfps = (1000 / frameInterval).floor().toInt();
|
| + }
|
| + GameHandler.frameMultiplier = frameInterval.toDouble() / GameHandler.FPSMS;
|
| +
|
| + GameHandler.frameStart = frameStart;
|
| +
|
| + if (!GameHandler.paused) {
|
| + window.requestAnimationFrame(GameHandler.doFrame);
|
| + }
|
| + if ((++totalFrames % 600) == 0) {
|
| + log('${totalFrames} frames; multiplier ${GameHandler.frameMultiplier}');
|
| + }
|
| + }
|
| +
|
| + void onRenderGame(CanvasRenderingContext2D ctx) {}
|
| +
|
| + bool isGameOver() => false;
|
| +}
|
| +
|
| +class AsteroidsMain extends GameMain {
|
| +
|
| + AsteroidsMain() : super() {
|
| + var attractorScene = new AttractorScene(this);
|
| +
|
| + // get the images graphics loading
|
| + var loader = new Preloader();
|
| + loader.addImage(_playerImg, 'player.png');
|
| + loader.addImage(_asteroidImgs[0], 'asteroid1.png');
|
| + loader.addImage(_asteroidImgs[1], 'asteroid2.png');
|
| + loader.addImage(_asteroidImgs[2], 'asteroid3.png');
|
| + loader.addImage(_asteroidImgs[3], 'asteroid4.png');
|
| + loader.addImage(_shieldImg, 'shield.png');
|
| + loader.addImage(_enemyshipImg, 'enemyship1.png');
|
| +
|
| + // The attactor scene is displayed first and responsible for allowing the
|
| + // player to start the game once all images have been loaded.
|
| + loader.onLoadCallback(() {
|
| + attractorScene.ready();
|
| + });
|
| +
|
| + // Generate the single player actor - available across all scenes.
|
| + player = new Player(
|
| + new Vector(GameHandler.width / 2, GameHandler.height / 2),
|
| + new Vector(0.0, 0.0),
|
| + 0.0);
|
| +
|
| + scenes.add(attractorScene);
|
| +
|
| + for (var i = 0; i < 12; i++){
|
| + var level = new GameScene(this, i+1);
|
| + scenes.add(level);
|
| + }
|
| +
|
| + scenes.add(new GameCompleted(this));
|
| +
|
| + // Set special end scene member value to a Game Over scene.
|
| + endScene = new GameOverScene(this);
|
| +
|
| + if (window.localStorage.containsKey(SCOREDBKEY)) {
|
| + highscore = int.parse(window.localStorage[SCOREDBKEY]);
|
| + }
|
| + // Perform prerender steps - create some bitmap graphics to use later.
|
| + GameHandler.bitmaps = new Prerenderer();
|
| + GameHandler.bitmaps.execute();
|
| + }
|
| +
|
| + Player player = null;
|
| + int lives = 0;
|
| + int score = 0;
|
| + int highscore = 0;
|
| + /** Background scrolling bitmap x position */
|
| + double backgroundX = 0.0;
|
| + /** Background starfield star list */
|
| + List starfield = [];
|
| +
|
| + void onRenderGame(CanvasRenderingContext2D ctx) {
|
| + // Setup canvas for a render pass and apply background
|
| + // draw a scrolling background image.
|
| + var w = GameHandler.width;
|
| + var h = GameHandler.height;
|
| + //var sourceRect = new Rect(backgroundX, 0, w, h);
|
| + //var destRect = new Rect(0, 0, w, h);
|
| + //ctx.drawImageToRect(_backgroundImg, destRect,
|
| + // sourceRect:sourceRect);
|
| + ctx.drawImageScaledFromSource(_backgroundImg,
|
| + backgroundX, 0, w, h, 0, 0, w, h);
|
| +
|
| + backgroundX += (GameHandler.frameMultiplier / 4.0);
|
| + if (backgroundX >= _backgroundImg.width / 2) {
|
| + backgroundX -= _backgroundImg.width / 2;
|
| + }
|
| + ctx.shadowBlur = 0;
|
| + }
|
| +
|
| + bool isGameOver() {
|
| + if (currentScene is GameScene) {
|
| + var gs = currentScene as GameScene;
|
| + return (lives == 0 && gs.effects != null && gs.effects.length == 0);
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * Update an actor position using its current velocity vector.
|
| + * Scale the vector by the frame multiplier - this is used to ensure
|
| + * all actors move the same distance over time regardles of framerate.
|
| + * Also handle traversing out of the coordinate space and back again.
|
| + */
|
| + void updateActorPosition(Actor actor) {
|
| + actor.position.add(actor.velocity.nscale(GameHandler.frameMultiplier));
|
| + actor.position.wrap(0, GameHandler.width - 1, 0, GameHandler.height - 1);
|
| + }
|
| +}
|
| +
|
| +class GameOverScene extends Scene {
|
| + var game, player;
|
| +
|
| + GameOverScene(this.game) :
|
| + super(false) {
|
| + interval = new Interval("GAME OVER", intervalRenderer);
|
| + player = game.player;
|
| + }
|
| +
|
| + bool isComplete() => true;
|
| +
|
| + void intervalRenderer(Interval interval, CanvasRenderingContext2D ctx) {
|
| + if (interval.framecounter++ == 0) {
|
| + if (game.score == game.highscore) {
|
| + window.localStorage[SCOREDBKEY] = game.score.toString();
|
| + }
|
| + }
|
| + if (interval.framecounter < 300) {
|
| + fillText(ctx, interval.label, "18pt Courier New",
|
| + GameHandler.width * 0.5 - 64, GameHandler.height*0.5 - 32, "white");
|
| + fillText(ctx, "Score: ${game.score}", "14pt Courier New",
|
| + GameHandler.width * 0.5 - 64, GameHandler.height*0.5, "white");
|
| + if (game.score == game.highscore) {
|
| + fillText(ctx, "New High Score!", "14pt Courier New",
|
| + GameHandler.width * 0.5 - 64, GameHandler.height*0.5 + 24, "white");
|
| + }
|
| + } else {
|
| + interval.complete = true;
|
| + }
|
| + }
|
| +}
|
| +
|
| +class GameScene extends Scene {
|
| + AsteroidsMain game;
|
| + int wave;
|
| + var player;
|
| + List actors = null;
|
| + List playerBullets = null;
|
| + List enemies = null;
|
| + List enemyBullets = null;
|
| + List effects = null;
|
| + List collectables = null;
|
| + int enemyShipCount = 0;
|
| + int enemyShipAdded = 0;
|
| + int scoredisplay = 0;
|
| + bool skipLevel = false;
|
| +
|
| + Input input;
|
| +
|
| + GameScene(this.game, this.wave)
|
| + : super(true) {
|
| + interval = new Interval("Wave ${wave}", intervalRenderer);
|
| + player = game.player;
|
| + input = new Input();
|
| + }
|
| +
|
| + void onInitScene() {
|
| + // Generate the actors and add the actor sub-lists to the main actor list.
|
| + actors = [];
|
| + enemies = [];
|
| + actors.add(enemies);
|
| + actors.add(playerBullets = []);
|
| + actors.add(enemyBullets = []);
|
| + actors.add(effects = []);
|
| + actors.add(collectables = []);
|
| +
|
| + // Reset player ready for game restart.
|
| + resetPlayerActor(wave != 1);
|
| +
|
| + // Randomly generate some asteroids.
|
| + var factor = 1.0 + ((wave - 1) * 0.075);
|
| + for (var i=1, j=(4 + wave); i < j; i++) {
|
| + enemies.add(generateAsteroid(factor));
|
| + }
|
| +
|
| + // Reset enemy ship count and last enemy added time.
|
| + enemyShipAdded = GameHandler.frameStart;
|
| + enemyShipCount = 0;
|
| +
|
| + // Reset interval flag.
|
| + interval.reset();
|
| + skipLevel = false;
|
| + }
|
| +
|
| + /** Restore the player to the game - reseting position etc. */
|
| + void resetPlayerActor(bool persistPowerUps) {
|
| + actors.add([player]);
|
| +
|
| + // Reset the player position.
|
| + player.position.x = GameHandler.width / 2;
|
| + player.position.y = GameHandler.height / 2;
|
| + player.velocity.x = 0.0;
|
| + player.velocity.y = 0.0;
|
| + player.heading = 0.0;
|
| + player.reset(persistPowerUps);
|
| +
|
| + // Reset keyboard input values.
|
| + input.reset();
|
| + }
|
| +
|
| + /** Scene before rendering event handler. */
|
| + void onBeforeRenderScene() {
|
| + // Handle key input.
|
| + if (input.left) {
|
| + // Rotate anti-clockwise.
|
| + player.heading -= 4 * GameHandler.frameMultiplier;
|
| + }
|
| + if (input.right) {
|
| + // Rotate clockwise.
|
| + player.heading += 4 * GameHandler.frameMultiplier;
|
| + }
|
| + if (input.thrust) {
|
| + player.thrust();
|
| + }
|
| + if (input.shield) {
|
| + if (!player.expired()) {
|
| + player.activateShield();
|
| + }
|
| + }
|
| + if (input.fireA) {
|
| + player.firePrimary(playerBullets);
|
| + }
|
| + if (input.fireB) {
|
| + player.fireSecondary(playerBullets);
|
| + }
|
| +
|
| + // Add an enemy every N frames (depending on wave factor).
|
| + // Later waves can have 2 ships on screen - earlier waves have one.
|
| + if (enemyShipCount <= (wave < 5 ? 0 : 1) &&
|
| + GameHandler.frameStart - enemyShipAdded > (20000 - (wave * 1024))) {
|
| + enemies.add(new EnemyShip(this, (wave < 3 ? 0 : randomInt(0, 1))));
|
| + enemyShipCount++;
|
| + enemyShipAdded = GameHandler.frameStart;
|
| + }
|
| +
|
| + // Update all actors using their current vector.
|
| + updateActors();
|
| + }
|
| +
|
| + /** Scene rendering event handler */
|
| + void onRenderScene(CanvasRenderingContext2D ctx) {
|
| + renderActors(ctx);
|
| +
|
| + if (Debug['collisionRadius']) {
|
| + renderCollisionRadius(ctx);
|
| + }
|
| +
|
| + // Render info overlay graphics.
|
| + renderOverlay(ctx);
|
| +
|
| + // Detect bullet collisions.
|
| + collisionDetectBullets();
|
| +
|
| + // Detect player collision with asteroids etc.
|
| + if (!player.expired()) {
|
| + collisionDetectPlayer();
|
| + } else {
|
| + // If the player died, then respawn after a short delay and
|
| + // ensure that they do not instantly collide with an enemy.
|
| + if (GameHandler.frameStart - player.killedOn > 3000) {
|
| + // Perform a test to check no ememy is close to the player.
|
| + var tooClose = false;
|
| + var playerPos =
|
| + new Vector(GameHandler.width * 0.5, GameHandler.height * 0.5);
|
| + for (var i=0, j=this.enemies.length; i<j; i++) {
|
| + var enemy = this.enemies[i];
|
| + if (playerPos.distance(enemy.position) < 80) {
|
| + tooClose = true;
|
| + break;
|
| + }
|
| + }
|
| + if (tooClose == false) {
|
| + resetPlayerActor(false);
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + bool isComplete() =>
|
| + (skipLevel || (enemies.length == 0 && effects.length == 0));
|
| +
|
| + void intervalRenderer(Interval interval, CanvasRenderingContext2D ctx) {
|
| + if (interval.framecounter++ < 100) {
|
| + fillText(ctx, interval.label, "18pt Courier New",
|
| + GameHandler.width*0.5 - 48, GameHandler.height*0.5 - 8, "white");
|
| + } else {
|
| + interval.complete = true;
|
| + }
|
| + }
|
| +
|
| + bool onAccelerometer(double x, double y, double z) {
|
| + if (input != null) {
|
| + input.shield =(x > 2.0);
|
| + input.thrust = (x < -1.0);
|
| + input.left = (y < -1.5);
|
| + input.right = (y > 1.5);
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + bool onMouseDownHandler(e) {
|
| + input.fireA = input.fireB = false;
|
| + if (e.clientX < GameHandler.width / 3) input.fireB = true;
|
| + else if (e.clientX > 2 * GameHandler.width / 3) input.fireA = true;
|
| + return true;
|
| + }
|
| +
|
| + bool onMouseUpHandler(e) {
|
| + input.fireA = input.fireB = false;
|
| + return true;
|
| + }
|
| +
|
| + bool onKeyDownHandler(int keyCode) {
|
| + log("In onKeyDownHandler, GameScene");
|
| + switch (keyCode) {
|
| + // Note: GLUT doesn't send key up events,
|
| + // so the emulator sends key events as down/up pairs,
|
| + // which is not what we want. So we have some special
|
| + // numeric key handlers here that are distinct for
|
| + // up and down to support use with GLUT.
|
| + case 52: // '4':
|
| + case Key.LEFT:
|
| + input.left = true;
|
| + return true;
|
| + case 54: // '6'
|
| + case Key.RIGHT:
|
| + input.right = true;
|
| + return true;
|
| + case 56: // '8'
|
| + case Key.UP:
|
| + input.thrust = true;
|
| + return true;
|
| + case 50: // '2'
|
| + case Key.DOWN:
|
| + case Key.SHIFT:
|
| + input.shield = true;
|
| + return true;
|
| + case 48: // '0'
|
| + case Key.SPACE:
|
| + input.fireA = true;
|
| + return true;
|
| + case Key.Z:
|
| + input.fireB = true;
|
| + return true;
|
| +
|
| + case Key.A:
|
| + if (Debug['enabled']) {
|
| + // generate an asteroid
|
| + enemies.add(generateAsteroid(1));
|
| + return true;
|
| + }
|
| + break;
|
| +
|
| + case Key.G:
|
| + if (Debug['enabled']) {
|
| + glowEffectOn = !glowEffectOn;
|
| + return true;
|
| + }
|
| + break;
|
| +
|
| + case Key.L:
|
| + if (Debug['enabled']) {
|
| + skipLevel = true;
|
| + return true;
|
| + }
|
| + break;
|
| +
|
| + case Key.E:
|
| + if (Debug['enabled']) {
|
| + enemies.add(new EnemyShip(this, randomInt(0, 1)));
|
| + return true;
|
| + }
|
| + break;
|
| +
|
| + case Key.ESC:
|
| + GameHandler.togglePause();
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + bool onKeyUpHandler(int keyCode) {
|
| + switch (keyCode) {
|
| + case 53: // '5'
|
| + input.left = false;
|
| + input.right = false;
|
| + input.thrust = false;
|
| + input.shield = false;
|
| + input.fireA = false;
|
| + input.fireB = false;
|
| + return true;
|
| +
|
| + case Key.LEFT:
|
| + input.left = false;
|
| + return true;
|
| + case Key.RIGHT:
|
| + input.right = false;
|
| + return true;
|
| + case Key.UP:
|
| + input.thrust = false;
|
| + return true;
|
| + case Key.DOWN:
|
| + case Key.SHIFT:
|
| + input.shield = false;
|
| + return true;
|
| + case Key.SPACE:
|
| + input.fireA = false;
|
| + return true;
|
| + case Key.Z:
|
| + input.fireB = false;
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * Randomly generate a new large asteroid. Ensures the asteroid is not
|
| + * generated too close to the player position!
|
| + */
|
| + Asteroid generateAsteroid(num speedFactor) {
|
| + while (true){
|
| + // perform a test to check it is not too close to the player
|
| + var apos = new Vector(random()*GameHandler.width,
|
| + random()*GameHandler.height);
|
| + if (player.position.distance(apos) > 125) {
|
| + var vec = new Vector( ((random()*2)-1)*speedFactor,
|
| + ((random()*2)-1)*speedFactor );
|
| + return new Asteroid(apos, vec, 4);
|
| + }
|
| + }
|
| + }
|
| +
|
| + /** Update the actors position based on current vectors and expiration. */
|
| + void updateActors() {
|
| + for (var i = 0, j = this.actors.length; i < j; i++) {
|
| + var actorList = this.actors[i];
|
| +
|
| + for (var n = 0; n < actorList.length; n++) {
|
| + var actor = actorList[n];
|
| +
|
| + // call onUpdate() event for each actor
|
| + actor.onUpdate(this);
|
| +
|
| + // expiration test first
|
| + if (actor.expired()) {
|
| + actorList.removeAt(n);
|
| + } else {
|
| + game.updateActorPosition(actor);
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Perform the operation needed to destory the player.
|
| + * Mark as killed as reduce lives, explosion effect and play sound.
|
| + */
|
| + void destroyPlayer() {
|
| + // Player destroyed by enemy bullet - remove from play.
|
| + player.kill();
|
| + game.lives--;
|
| + var boom =
|
| + new PlayerExplosion(player.position.clone(), player.velocity.clone());
|
| + effects.add(boom);
|
| + soundManager.play('big_boom');
|
| + }
|
| +
|
| + /**
|
| + * Detect player collisions with various actor classes
|
| + * including Asteroids, Enemies, bullets and collectables
|
| + */
|
| + void collisionDetectPlayer() {
|
| + var playerRadius = player.radius;
|
| + var playerPos = player.position;
|
| +
|
| + // Test circle intersection with each asteroid/enemy ship.
|
| + for (var n = 0, m = enemies.length; n < m; n++) {
|
| + var enemy = enemies[n];
|
| +
|
| + // Calculate distance between the two circles.
|
| + if (playerPos.distance(enemy.position) <= playerRadius + enemy.radius) {
|
| + // Collision detected.
|
| + if (player.isShieldActive()) {
|
| + // Remove thrust from the player vector due to collision.
|
| + player.velocity.scale(0.75);
|
| +
|
| + // Destroy the enemy - the player is invincible with shield up!
|
| + enemy.hit(-1);
|
| + destroyEnemy(enemy, player.velocity, true);
|
| + } else if (!Debug['invincible']) {
|
| + destroyPlayer();
|
| + }
|
| + }
|
| + }
|
| +
|
| + // Test intersection with each enemy bullet.
|
| + for (var i = 0; i < enemyBullets.length; i++) {
|
| + var bullet = enemyBullets[i];
|
| +
|
| + // Calculate distance between the two circles.
|
| + if (playerPos.distance(bullet.position) <= playerRadius + bullet.radius) {
|
| + // Collision detected.
|
| + if (player.isShieldActive()) {
|
| + // Remove this bullet from the actor list as it has been destroyed.
|
| + enemyBullets.removeAt(i);
|
| + } else if (!Debug['invincible']) {
|
| + destroyPlayer();
|
| + }
|
| + }
|
| + }
|
| +
|
| + // Test intersection with each collectable.
|
| + for (var i = 0; i < collectables.length; i++) {
|
| + var item = collectables[i];
|
| +
|
| + // Calculate distance between the two circles.
|
| + if (playerPos.distance(item.position) <= playerRadius + item.radius) {
|
| + // Collision detected - remove item from play and activate it.
|
| + collectables.removeAt(i);
|
| + item.collected(game, player, this);
|
| +
|
| + soundManager.play('powerup');
|
| + }
|
| + }
|
| + }
|
| +
|
| + /** Detect bullet collisions with asteroids and enemy actors. */
|
| + void collisionDetectBullets() {
|
| + var i;
|
| + // Collision detect player bullets with asteroids and enemies.
|
| + for (i = 0; i < playerBullets.length; i++) {
|
| + var bullet = playerBullets[i];
|
| + var bulletRadius = bullet.radius;
|
| + var bulletPos = bullet.position;
|
| +
|
| + // Test circle intersection with each enemy actor.
|
| + var n, m = enemies.length, z;
|
| + for (n = 0; n < m; n++) {
|
| + var enemy = enemies[n];
|
| +
|
| + // Test the distance against the two radius combined.
|
| + if (bulletPos.distance(enemy.position) <= bulletRadius + enemy.radius){
|
| + // intersection detected!
|
| +
|
| + // Test for area effect bomb weapon.
|
| + var effectRad = bullet.effectRadius;
|
| + if (effectRad == 0) {
|
| + // Impact the enemy with the bullet.
|
| + if (enemy.hit(bullet.power)) {
|
| + // Destroy the enemy under the bullet.
|
| + destroyEnemy(enemy, bullet.velocity, true);
|
| + // Randomly release a power up.
|
| + generatePowerUp(enemy);
|
| + } else {
|
| + // Add a bullet impact particle effect to show the hit.
|
| + var effect =
|
| + new PlayerBulletImpact(bullet.position, bullet.velocity);
|
| + effects.add(effect);
|
| + }
|
| + } else {
|
| + // Inform enemy it has been hit by a instant kill weapon.
|
| + enemy.hit(-1);
|
| + generatePowerUp(enemy);
|
| +
|
| + // Add a big explosion actor at the area weapon position and vector.
|
| + var comboCount = 1;
|
| + var boom = new Explosion(
|
| + bullet.position.clone(),
|
| + bullet.velocity.nscale(0.5), 5);
|
| + effects.add(boom);
|
| +
|
| + // Destroy the enemy.
|
| + destroyEnemy(enemy, bullet.velocity, true);
|
| +
|
| + // Wipe out nearby enemies under the weapon effect radius
|
| + // take the length of the enemy actor list here - so we don't
|
| + // kill off -all- baby asteroids - so some elements of the original
|
| + // survive.
|
| + for (var x = 0, z = this.enemies.length, e; x < z; x++) {
|
| + e = enemies[x];
|
| +
|
| + // test the distance against the two radius combined
|
| + if (bulletPos.distance(e.position) <= effectRad + e.radius) {
|
| + e.hit(-1);
|
| + generatePowerUp(e);
|
| + destroyEnemy(e, bullet.velocity, true);
|
| + comboCount++;
|
| + }
|
| + }
|
| +
|
| + // Special score and indicator for "combo" detonation.
|
| + if (comboCount > 4) {
|
| + // Score bonus based on combo size.
|
| + var inc = comboCount * 1000 * wave;
|
| + game.score += inc;
|
| +
|
| + // Generate a special effect indicator at the destroyed
|
| + // enemy position.
|
| + var vec = new Vector(0, -3.0);
|
| + var effect = new ScoreIndicator(
|
| + new Vector(enemy.position.x,
|
| + enemy.position.y - (enemy.size * 8)),
|
| + vec.add(enemy.velocity.nscale(0.5)),
|
| + inc, 16, 'COMBO X ${comboCount}', 'rgb(255,255,55)', 1000);
|
| + effects.add(effect);
|
| +
|
| + // Generate a powerup to reward the player for the combo.
|
| + generatePowerUp(enemy, true);
|
| + }
|
| + }
|
| +
|
| + // Remove this bullet from the actor list as it has been destroyed.
|
| + playerBullets.removeAt(i);
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + // collision detect enemy bullets with asteroids
|
| + for (i = 0; i < enemyBullets.length; i++) {
|
| + var bullet = enemyBullets[i];
|
| + var bulletRadius = bullet.radius;
|
| + var bulletPos = bullet.position;
|
| +
|
| + // test circle intersection with each enemy actor
|
| + var n, m = enemies.length, z;
|
| + for (n = 0; n < m; n++) {
|
| + var enemy = enemies[n];
|
| +
|
| + if (enemy is Asteroid) {
|
| + if (bulletPos.distance(enemy.position) <=
|
| + bulletRadius + enemy.radius) {
|
| + // Impact the enemy with the bullet.
|
| + if (enemy.hit(1)) {
|
| + // Destroy the enemy under the bullet.
|
| + destroyEnemy(enemy, bullet.velocity, false);
|
| + } else {
|
| + // Add a bullet impact particle effect to show the hit.
|
| + var effect = new EnemyBulletImpact(bullet.position,
|
| + bullet.velocity);
|
| + effects.add(effect);
|
| + }
|
| +
|
| + // Remove this bullet from the actor list as it has been destroyed.
|
| + enemyBullets.removeAt(i);
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + /** Randomly generate a power up to reward the player */
|
| + void generatePowerUp(EnemyActor enemy, [bool force = false]) {
|
| + if (collectables.length < 5 &&
|
| + (force || randomInt(0, ((enemy is Asteroid) ? 25 : 1)) == 0)) {
|
| + // Apply a small random vector in the direction of travel
|
| + // rotate by slightly randomized enemy heading.
|
| + var vec = enemy.velocity.clone();
|
| + var t = new Vector(0.0, -(random() * 2));
|
| + t.rotate(enemy.velocity.theta() * (random() * Math.PI));
|
| + vec.add(t);
|
| +
|
| + // Add a power up to the collectables list.
|
| + collectables.add(new PowerUp(
|
| + new Vector(enemy.position.x, enemy.position.y - (enemy.size * 8)),
|
| + vec));
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Blow up an enemy.
|
| + *
|
| + * An asteroid may generate new baby asteroids and leave an explosion
|
| + * in the wake.
|
| + *
|
| + * Also applies the score for the destroyed item.
|
| + *
|
| + * @param enemy {Game.EnemyActor} The enemy to destory and add score for
|
| + * @param parentVector {Vector} The vector of the item that hit the enemy
|
| + * @param player {boolean} If true, the player was the destroyer
|
| + */
|
| + void destroyEnemy(EnemyActor enemy, Vector parentVector, player) {
|
| + if (enemy is Asteroid) {
|
| + soundManager.play('asteroid_boom${randomInt(1,4)}');
|
| +
|
| + // generate baby asteroids
|
| + generateBabyAsteroids(enemy, parentVector);
|
| +
|
| + // add an explosion at the asteriod position and vector
|
| + var boom = new AsteroidExplosion(
|
| + enemy.position.clone(), enemy.velocity.clone(), enemy);
|
| + effects.add(boom);
|
| +
|
| + if (player!= null) {
|
| + // increment score based on asteroid size
|
| + var inc = ((5 - enemy.size) * 4) * 100 * wave;
|
| + game.score += inc;
|
| +
|
| + // generate a score effect indicator at the destroyed enemy position
|
| + var vec = new Vector(0, -1.5).add(enemy.velocity.nscale(0.5));
|
| + var effect = new ScoreIndicator(
|
| + new Vector(enemy.position.x, enemy.position.y -
|
| + (enemy.size * 8)), vec, inc);
|
| + effects.add(effect);
|
| + }
|
| + } else if (enemy is EnemyShip) {
|
| + soundManager.play('asteroid_boom1');
|
| +
|
| + // add an explosion at the enemy ship position and vector
|
| + var boom = new EnemyExplosion(enemy.position.clone(),
|
| + enemy.velocity.clone(), enemy);
|
| + effects.add(boom);
|
| +
|
| + if (player != null) {
|
| + // increment score based on asteroid size
|
| + var inc = 2000 * wave * (enemy.size + 1);
|
| + game.score += inc;
|
| +
|
| + // generate a score effect indicator at the destroyed enemy position
|
| + var vec = new Vector(0, -1.5).add(enemy.velocity.nscale(0.5));
|
| + var effect = new ScoreIndicator(
|
| + new Vector(enemy.position.x, enemy.position.y - 16),
|
| + vec, inc);
|
| + effects.add(effect);
|
| + }
|
| +
|
| + // decrement scene ship count
|
| + enemyShipCount--;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Generate a number of baby asteroids from a detonated parent asteroid.
|
| + * The number and size of the generated asteroids are based on the parent
|
| + * size. Some of the momentum of the parent vector (e.g. impacting bullet)
|
| + * is applied to the new asteroids.
|
| + */
|
| + void generateBabyAsteroids(Asteroid asteroid, Vector parentVector) {
|
| + // generate some baby asteroid(s) if bigger than the minimum size
|
| + if (asteroid.size > 1) {
|
| + var xc=randomInt(asteroid.size ~/ 2, asteroid.size - 1);
|
| + for (var x=0; x < xc; x++) {
|
| + var babySize = randomInt(1, asteroid.size - 1);
|
| +
|
| + var vec = asteroid.velocity.clone();
|
| +
|
| + // apply a small random vector in the direction of travel
|
| + var t = new Vector(0.0, -random());
|
| +
|
| + // rotate vector by asteroid current heading - slightly randomized
|
| + t.rotate(asteroid.velocity.theta() * (random() * Math.PI));
|
| + vec.add(t);
|
| +
|
| + // add the scaled parent vector - to give some momentum from the impact
|
| + vec.add(parentVector.nscale(0.2));
|
| +
|
| + // create the asteroid - slightly offset from the centre of the old one
|
| + var baby = new Asteroid(
|
| + new Vector(asteroid.position.x + (random()*5)-2.5,
|
| + asteroid.position.y + (random()*5)-2.5),
|
| + vec, babySize, asteroid.type);
|
| + enemies.add(baby);
|
| + }
|
| + }
|
| + }
|
| +
|
| + /** Render each actor to the canvas. */
|
| + void renderActors(CanvasRenderingContext2D ctx){
|
| + for (var i = 0, j = actors.length; i < j; i++) {
|
| + // walk each sub-list and call render on each object
|
| + var actorList = actors[i];
|
| +
|
| + for (var n = actorList.length - 1; n >= 0; n--) {
|
| + actorList[n].onRender(ctx);
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * DEBUG - Render the radius of the collision detection circle around
|
| + * each actor.
|
| + */
|
| + void renderCollisionRadius(CanvasRenderingContext2D ctx) {
|
| + ctx.save();
|
| + ctx.strokeStyle = "rgb(255,0,0)";
|
| + ctx.lineWidth = 0.5;
|
| + ctx.shadowBlur = 0;
|
| +
|
| + for (var i = 0, j = actors.length; i < j; i++) {
|
| + var actorList = actors[i];
|
| +
|
| + for (var n = actorList.length - 1, actor; n >= 0; n--) {
|
| + actor = actorList[n];
|
| + if (actor.radius) {
|
| + ctx.beginPath();
|
| + ctx.arc(actor.position.x, actor.position.y, actor.radius, 0,
|
| + TWOPI, true);
|
| + ctx.closePath();
|
| + ctx.stroke();
|
| + }
|
| + }
|
| + }
|
| + ctx.restore();
|
| + }
|
| +
|
| + /**
|
| + * Render player information HUD overlay graphics.
|
| + *
|
| + * @param ctx {object} Canvas rendering context
|
| + */
|
| + void renderOverlay(CanvasRenderingContext2D ctx) {
|
| + ctx.save();
|
| + ctx.shadowBlur = 0;
|
| +
|
| + // energy bar (100 pixels across, scaled down from player energy max)
|
| + ctx.strokeStyle = "rgb(50,50,255)";
|
| + ctx.strokeRect(4, 4, 101, 6);
|
| + ctx.fillStyle = "rgb(100,100,255)";
|
| + var energy = player.energy;
|
| + if (energy > player.ENERGY_INIT) {
|
| + // the shield is on for "free" briefly when he player respawns
|
| + energy = player.ENERGY_INIT;
|
| + }
|
| + ctx.fillRect(5, 5, (energy / (player.ENERGY_INIT / 100)), 5);
|
| +
|
| + // lives indicator graphics
|
| + for (var i=0; i<game.lives; i++) {
|
| + drawScaledImage(ctx, _playerImg, 0, 0, 64,
|
| + 350+(i*20), 0, 16);
|
| +
|
| + // score display - update towards the score in increments to animate it
|
| + var score = game.score;
|
| + var inc = (score - scoredisplay) ~/ 10;
|
| + scoredisplay += inc;
|
| + if (scoredisplay > score) {
|
| + scoredisplay = score;
|
| + }
|
| + var sscore = scoredisplay.ceil().toString();
|
| + // pad with zeros
|
| + for (var i=0, j=8-sscore.length; i<j; i++) {
|
| + sscore = "0${sscore}";
|
| + }
|
| + fillText(ctx, sscore, "12pt Courier New", 120, 12, "white");
|
| +
|
| + // high score
|
| + // TODO: add method for incrementing score so this is not done here
|
| + if (score > game.highscore) {
|
| + game.highscore = score;
|
| + }
|
| + sscore = game.highscore.toString();
|
| + // pad with zeros
|
| + for (var i=0, j=8-sscore.length; i<j; i++) {
|
| + sscore = "0${sscore}";
|
| + }
|
| + fillText(ctx, "HI: ${sscore}", "12pt Courier New", 220, 12, "white");
|
| +
|
| + // debug output
|
| + if (Debug['fps']) {
|
| + fillText(ctx, "FPS: ${GameHandler.maxfps}", "12pt Courier New",
|
| + 0, GameHandler.height - 2, "lightblue");
|
| + }
|
| + }
|
| + ctx.restore();
|
| + }
|
| +}
|
| +
|
| +class Interval {
|
| + String label;
|
| + Function intervalRenderer;
|
| + int framecounter = 0;
|
| + bool complete = false;
|
| +
|
| + Interval([this.label = null, this.intervalRenderer = null]);
|
| +
|
| + void reset() {
|
| + framecounter = 0;
|
| + complete = false;
|
| + }
|
| +}
|
| +
|
| +class Bullet extends ShortLivedActor {
|
| +
|
| + Bullet(Vector position, Vector velocity,
|
| + [this.heading = 0.0, int lifespan = 1300])
|
| + : super(position, velocity, lifespan) {
|
| + }
|
| +
|
| + const BULLET_WIDTH = 2;
|
| + const BULLET_HEIGHT = 6;
|
| + const FADE_LENGTH = 200;
|
| +
|
| + double heading;
|
| + int _power = 1;
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + // hack to stop draw under player graphic
|
| + if (frameStart - start > 40) {
|
| + ctx.save();
|
| + ctx.globalCompositeOperation = "lighter";
|
| + ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
|
| + // rotate the bullet bitmap into the correct heading
|
| + ctx.translate(position.x, position.y);
|
| + ctx.rotate(heading * RAD);
|
| + // TODO(gram) - figure out how to get rid of the vector art so we don't
|
| + // need the [0] below.
|
| + ctx.drawImage(GameHandler.bitmaps.images["bullet"],
|
| + -(BULLET_WIDTH + GLOWSHADOWBLUR*2)*0.5,
|
| + -(BULLET_HEIGHT + GLOWSHADOWBLUR*2)*0.5);
|
| + ctx.restore();
|
| + }
|
| + }
|
| +
|
| + /** Area effect weapon radius - zero for primary bullets. */
|
| + get effectRadius => 0;
|
| +
|
| + // approximate based on average between width and height
|
| + get radius => 4;
|
| +
|
| + get power => _power;
|
| +}
|
| +
|
| +/**
|
| + * Player BulletX2 actor class. Used by the TwinCannons primary weapon.
|
| + */
|
| +class BulletX2 extends Bullet {
|
| +
|
| + BulletX2(Vector position, Vector vector, double heading)
|
| + : super(position, vector, heading, 1750) {
|
| + _power = 2;
|
| + }
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + // hack to stop draw under player graphic
|
| + if (frameStart - start > 40) {
|
| + ctx.save();
|
| + ctx.globalCompositeOperation = "lighter";
|
| + ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
|
| + // rotate the bullet bitmap into the correct heading
|
| + ctx.translate(position.x, position.y);
|
| + ctx.rotate(heading * RAD);
|
| + ctx.drawImage(GameHandler.bitmaps.images["bulletx2"],
|
| + -(BULLET_WIDTH + GLOWSHADOWBLUR*4) / 2,
|
| + -(BULLET_HEIGHT + GLOWSHADOWBLUR*2) / 2);
|
| + ctx.restore();
|
| + }
|
| + }
|
| +
|
| + get radius => BULLET_HEIGHT;
|
| +}
|
| +
|
| +class Bomb extends Bullet {
|
| + Bomb(Vector position, Vector velocity)
|
| + : super(position, velocity, 0.0, 3000);
|
| +
|
| + const BOMB_RADIUS = 4.0;
|
| + const FADE_LENGTH = 200;
|
| + const EFFECT_RADIUS = 45;
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + ctx.save();
|
| + ctx.globalCompositeOperation = "lighter";
|
| + ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
|
| + ctx.translate(position.x, position.y);
|
| + ctx.rotate((frameStart % (360*32)) / 32);
|
| + var scale = fadeValue(1.0, FADE_LENGTH);
|
| + if (scale <= 0) scale = 0.01;
|
| + ctx.scale(scale, scale);
|
| + ctx.drawImage(GameHandler.bitmaps.images["bomb"],
|
| + -(BOMB_RADIUS + GLOWSHADOWBLUR),
|
| + -(BOMB_RADIUS + GLOWSHADOWBLUR));
|
| + ctx.restore();
|
| + }
|
| +
|
| + get effectRadius => EFFECT_RADIUS;
|
| + get radius => fadeValue(BOMB_RADIUS, FADE_LENGTH);
|
| +}
|
| +
|
| +class EnemyBullet extends Bullet {
|
| + EnemyBullet(Vector position, Vector velocity)
|
| + : super(position, velocity, 0.0, 2800);
|
| +
|
| + const BULLET_RADIUS = 4.0;
|
| + const FADE_LENGTH = 200;
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + ctx.save();
|
| + ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
|
| + ctx.globalCompositeOperation = "lighter";
|
| + ctx.translate(position.x, position.y);
|
| + ctx.rotate((frameStart % (360*64)) / 64);
|
| + var scale = fadeValue(1.0, FADE_LENGTH);
|
| + if (scale <= 0) scale = 0.01;
|
| + ctx.scale(scale, scale);
|
| + ctx.drawImage(GameHandler.bitmaps.images["enemybullet"],
|
| + -(BULLET_RADIUS + GLOWSHADOWBLUR),
|
| + -(BULLET_RADIUS + GLOWSHADOWBLUR));
|
| + ctx.restore();
|
| + }
|
| +
|
| + get radius => fadeValue(BULLET_RADIUS, FADE_LENGTH) + 1;
|
| +}
|
| +
|
| +class Particle extends ShortLivedActor {
|
| + int size;
|
| + int type;
|
| + int fadelength;
|
| + String color;
|
| + double rotate;
|
| + double rotationv;
|
| +
|
| + Particle(Vector position, Vector velocity, this.size, this.type,
|
| + int lifespan, this.fadelength,
|
| + [this.color = Colors.PARTICLE])
|
| + : super(position, velocity, lifespan) {
|
| +
|
| + // randomize rotation speed and angle for line particle
|
| + if (type == 1) {
|
| + rotate = random() * TWOPI;
|
| + rotationv = (random() - 0.5) * 0.5;
|
| + }
|
| + }
|
| +
|
| + bool update() {
|
| + position.add(velocity);
|
| + return !expired();
|
| + }
|
| +
|
| + void render(CanvasRenderingContext2D ctx) {
|
| + ctx.globalAlpha = fadeValue(1.0, fadelength);
|
| + switch (type) {
|
| + case 0: // point (prerendered image)
|
| + ctx.translate(position.x, position.y);
|
| + ctx.drawImage(
|
| + GameHandler.bitmaps.images["points_${color}"][size], 0, 0);
|
| + break;
|
| + // TODO: prerender a glowing line to use as the particle!
|
| + case 1: // line
|
| + ctx.translate(position.x, position.y);
|
| + var s = size;
|
| + ctx.rotate(rotate);
|
| + this.rotate += rotationv;
|
| + ctx.strokeStyle = color;
|
| + ctx.lineWidth = 1.5;
|
| + ctx.beginPath();
|
| + ctx.moveTo(-s, -s);
|
| + ctx.lineTo(s, s);
|
| + ctx.closePath();
|
| + ctx.stroke();
|
| + break;
|
| + case 2: // smudge (prerendered image)
|
| + var offset = (size + 1) << 2;
|
| + renderImage(ctx,
|
| + GameHandler.bitmaps.images["smudges_${color}"][size],
|
| + 0, 0, (size + 1) << 3,
|
| + position.x - offset, position.y - offset, (size + 1) << 3);
|
| + break;
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Particle emitter effect actor class.
|
| + *
|
| + * A simple particle emitter, that does not recycle particles, but sets itself
|
| + * as expired() once all child particles have expired.
|
| + *
|
| + * Requires a function known as the emitter that is called per particle
|
| + * generated.
|
| + */
|
| +class ParticleEmitter extends Actor {
|
| +
|
| + List<Particle> particles;
|
| +
|
| + ParticleEmitter(Vector position, Vector velocity)
|
| + : super(position, velocity);
|
| +
|
| + Particle emitter() {}
|
| +
|
| + void init(count) {
|
| + // generate particles based on the supplied emitter function
|
| + particles = [];
|
| + for (var i = 0; i < count; i++) {
|
| + particles.add(emitter());
|
| + }
|
| + }
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + ctx.save();
|
| + ctx.shadowBlur = 0;
|
| + ctx.globalCompositeOperation = "lighter";
|
| + for (var i=0, particle; i < particles.length; i++) {
|
| + particle = particles[i];
|
| +
|
| + // update particle and test for lifespan
|
| + if (particle.update()) {
|
| + ctx.save();
|
| + particle.render(ctx);
|
| + ctx.restore();
|
| + } else {
|
| + // particle no longer alive, remove from list
|
| + particles.removeAt(i);
|
| + }
|
| + }
|
| + ctx.restore();
|
| + }
|
| +
|
| + bool expired() => (particles.length == 0);
|
| +}
|
| +
|
| +class AsteroidExplosion extends ParticleEmitter {
|
| + var asteroid;
|
| +
|
| + AsteroidExplosion(Vector position, Vector vector, this.asteroid)
|
| + : super(position, vector) {
|
| + init(asteroid.size*2);
|
| + }
|
| +
|
| + Particle emitter() {
|
| + // Randomise radial direction vector - speed and angle, then add parent
|
| + // vector.
|
| + var pos = position.clone();
|
| + if (random() < 0.5) {
|
| + var t = new Vector(0, randomInt(5, 10));
|
| + t.rotate(random() * TWOPI).add(velocity);
|
| + return new Particle(pos, t, (random() * 4).floor(), 0, 400, 300);
|
| + } else {
|
| + var t = new Vector(0, randomInt(1, 3));
|
| + t.rotate(random() * TWOPI).add(velocity);
|
| + return new Particle(pos, t,
|
| + (random() * 4).floor() + asteroid.size, 2, 500, 250);
|
| + }
|
| + }
|
| +}
|
| +
|
| +class PlayerExplosion extends ParticleEmitter {
|
| + PlayerExplosion(Vector position, Vector vector)
|
| + : super(position, vector) {
|
| + init(12);
|
| + }
|
| +
|
| + Particle emitter() {
|
| + // Randomise radial direction vector - speed and angle, then add
|
| + // parent vector.
|
| + var pos = position.clone();
|
| + if (random() < 0.5){
|
| + var t = new Vector(0, randomInt(5, 10));
|
| + t.rotate(random() * TWOPI).add(velocity);
|
| + return new Particle(pos, t, (random() * 4).floor(), 0, 400, 300);
|
| + } else {
|
| + var t = new Vector(0, randomInt(1, 3));
|
| + t.rotate(random() * TWOPI).add(velocity);
|
| + return new Particle(pos, t, (random() * 4).floor() + 2, 2, 500, 250);
|
| + }
|
| + }
|
| +}
|
| +
|
| +/** Enemy particle based explosion - Particle effect actor class. */
|
| +class EnemyExplosion extends ParticleEmitter {
|
| + var enemy;
|
| + EnemyExplosion(Vector position, Vector vector, this.enemy)
|
| + : super(position, vector) {
|
| + init(8);
|
| + }
|
| +
|
| + Particle emitter() {
|
| + // randomise radial direction vector - speed and angle, then
|
| + // add parent vector.
|
| + var pos = position.clone();
|
| + if (random() < 0.5) {
|
| + var t = new Vector(0, randomInt(5, 10));
|
| + t.rotate(random() * TWOPI).add(velocity);
|
| + return new Particle(pos, t, (random() * 4).floor(), 0,
|
| + 400, 300, Colors.ENEMY_SHIP);
|
| + } else {
|
| + var t = new Vector(0, randomInt(1, 3));
|
| + t.rotate(random() * 2 * TWOPI).add(velocity);
|
| + return new Particle(pos, t,
|
| + (random() * 4).floor() + (enemy.size == 0 ? 2 : 0), 2,
|
| + 500, 250, Colors.ENEMY_SHIP);
|
| + }
|
| + }
|
| +}
|
| +
|
| +class Explosion extends EffectActor {
|
| +/**
|
| + * Basic explosion effect actor class.
|
| + *
|
| + * TODO: replace all instances of this with particle effects
|
| + * - this is still usedby the smartbomb
|
| + */
|
| + Explosion(Vector position, Vector vector, this.size)
|
| + : super(position, vector, FADE_LENGTH);
|
| +
|
| + static const FADE_LENGTH = 300;
|
| +
|
| + num size = 0;
|
| +
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + // fade out
|
| + var brightness = (effectValue(255.0)).floor(),
|
| + rad = effectValue(size * 8.0),
|
| + rgb = brightness.toString();
|
| + ctx.save();
|
| + ctx.globalAlpha = 0.75;
|
| + ctx.fillStyle = "rgb(${rgb},0,0)";
|
| + ctx.beginPath();
|
| + ctx.arc(position.x, position.y, rad, 0, TWOPI, true);
|
| + ctx.closePath();
|
| + ctx.fill();
|
| + ctx.restore();
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Player bullet impact effect - Particle effect actor class.
|
| + * Used when an enemy is hit by player bullet but not destroyed.
|
| + */
|
| +class PlayerBulletImpact extends ParticleEmitter {
|
| + PlayerBulletImpact(Vector position, Vector vector)
|
| + : super(position, vector) {
|
| + init(5);
|
| + }
|
| +
|
| + Particle emitter() {
|
| + // slightly randomise vector angle - then add parent vector
|
| + var t = velocity.nscale(0.75 + random() * 0.5);
|
| + t.rotate(random() * PIO4 - PIO8);
|
| + return new Particle(position.clone(), t,
|
| + (random() * 4).floor(), 0, 250, 150, Colors.GREEN_LASER);
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Enemy bullet impact effect - Particle effect actor class.
|
| + * Used when an enemy is hit by player bullet but not destroyed.
|
| + */
|
| +class EnemyBulletImpact extends ParticleEmitter {
|
| + EnemyBulletImpact(Vector position , Vector vector)
|
| + : super(position, vector) {
|
| + init(5);
|
| + }
|
| +
|
| + Particle emitter() {
|
| + // slightly randomise vector angle - then add parent vector
|
| + var t = velocity.nscale(0.75 + random() * 0.5);
|
| + t.rotate(random() * PIO4 - PIO8);
|
| + return new Particle(position.clone(), t,
|
| + (random() * 4).floor(), 0, 250, 150, Colors.ENEMY_SHIP);
|
| + }
|
| +}
|
| +
|
| +class Player extends SpriteActor {
|
| + Player(Vector position, Vector vector, this.heading)
|
| + : super(position, vector) {
|
| + energy = ENERGY_INIT;
|
| +
|
| + // setup SpriteActor values - used for shield sprite
|
| + animImage = _shieldImg;
|
| + animLength = SHIELD_ANIM_LENGTH;
|
| +
|
| + // setup weapons
|
| + primaryWeapons = {};
|
| + }
|
| +
|
| + const MAX_PLAYER_VELOCITY = 8.0;
|
| + const PLAYER_RADIUS = 9;
|
| + const SHIELD_RADIUS = 14;
|
| + const SHIELD_ANIM_LENGTH = 100;
|
| + const SHIELD_MIN_PULSE = 20;
|
| + const ENERGY_INIT = 400;
|
| + const THRUST_DELAY_MS = 100;
|
| + const BOMB_RECHARGE_MS = 800;
|
| + const BOMB_ENERGY = 80;
|
| +
|
| + double heading = 0.0;
|
| +
|
| + /** Player energy (shield and bombs). */
|
| + num energy = 0;
|
| +
|
| + /** Player shield active counter. */
|
| + num shieldCounter = 0;
|
| +
|
| + bool alive = true;
|
| + Map primaryWeapons = null;
|
| +
|
| + /** Bomb fire recharging counter. */
|
| + num bombRecharge = 0;
|
| +
|
| + /** Engine thrust recharge counter. */
|
| + num thrustRecharge = 0;
|
| +
|
| + /** True if the engine thrust graphics should be rendered next frame. */
|
| + bool engineThrust = false;
|
| +
|
| + /**
|
| + * Time that the player was killed - to cause a delay before respawning
|
| + * the player
|
| + */
|
| + num killedOn = 0;
|
| +
|
| + bool fireWhenShield = false;
|
| +
|
| + /** Player rendering method
|
| + *
|
| + * @param ctx {object} Canvas rendering context
|
| + */
|
| + void onRender(CanvasRenderingContext2D ctx) {
|
| + var headingRad = heading * RAD;
|
| +
|
| + // render engine thrust?
|
| + if (engineThrust) {
|
| + ctx.save();
|
| + ctx.translate(position.x, position.y);
|
| + ctx.rotate(headingRad);
|
| + ctx.globalAlpha = 0.5 + random() * 0.5;
|
| + ctx.globalCompositeOperation = "lighter";
|
| + ctx.fillStyle = Colors.PLAYER_THRUST;
|
| + ctx.beginPath();
|
| + ctx.moveTo(-5, 8);
|
| + ctx.lineTo(5, 8);
|
| + ctx.lineTo(0, 18 + random() * 6);
|
| + ctx.closePath();
|
| + ctx.fill();
|
| + ctx.restore();
|
| + engineThrust = false;
|
| + }
|
| +
|
| + // render player graphic
|
| + var size = (PLAYER_RADIUS * 2) + 6;
|
| + // normalise the player heading to 0-359 degrees
|
| + // then locate the correct frame in the sprite strip -
|
| + // an image for each 4 degrees of rotation
|
| + var normAngle = heading.floor() % 360;
|
| + if (normAngle < 0) {
|
| + normAngle = 360 + normAngle;
|
| + }
|
| + ctx.save();
|
| + drawScaledImage(ctx, _playerImg,
|
| + 0, (normAngle / 4).floor() * 64, 64,
|
| + position.x - (size / 2), position.y - (size / 2), size);
|
| + ctx.restore();
|
| +
|
| + // shield up? if so render a shield graphic around the ship
|
| + if (shieldCounter > 0 && energy > 0) {
|
| + // render shield graphic bitmap
|
| + ctx.save();
|
| + ctx.translate(position.x, position.y);
|
| + ctx.rotate(headingRad);
|
| + renderSprite(ctx, -SHIELD_RADIUS-1,
|
| + -SHIELD_RADIUS-1, (SHIELD_RADIUS * 2) + 2);
|
| + ctx.restore();
|
| +
|
| + shieldCounter--;
|
| + energy -= 1.5;
|
| + }
|
| + }
|
| +
|
| + /** Execute player forward thrust request. */
|
| + void thrust() {
|
| + // now test we did not thrust too recently, based on time since last thrust
|
| + // request - ensures same thrust at any framerate
|
| + if (frameStart - thrustRecharge > THRUST_DELAY_MS) {
|
| + // update last thrust time
|
| + thrustRecharge = frameStart;
|
| +
|
| + // generate a small thrust vector
|
| + var t = new Vector(0.0, -0.5);
|
| +
|
| + // rotate thrust vector by player current heading
|
| + t.rotate(heading * RAD);
|
| +
|
| + // add player thrust vector to position
|
| + velocity.add(t);
|
| +
|
| + // player can't exceed maximum velocity - scale vector down if
|
| + // this occurs - do this rather than not adding the thrust at all
|
| + // otherwise the player cannot turn and thrust at max velocity
|
| + if (velocity.length() > MAX_PLAYER_VELOCITY) {
|
| + velocity.scale(MAX_PLAYER_VELOCITY / velocity.length());
|
| + }
|
| + }
|
| + // mark so that we know to render engine thrust graphics
|
| + engineThrust = true;
|
| + }
|
| +
|
| + /**
|
| + * Execute player active shield request.
|
| + * If energy remaining the shield will be briefly applied.
|
| + */
|
| + void activateShield() {
|
| + // ensure shield stays up for a brief pulse between key presses!
|
| + if (energy >= SHIELD_MIN_PULSE) {
|
| + shieldCounter = SHIELD_MIN_PULSE;
|
| + }
|
| + }
|
| +
|
| + bool isShieldActive() => (shieldCounter > 0 && energy > 0);
|
| +
|
| + get radius => (isShieldActive() ? SHIELD_RADIUS : PLAYER_RADIUS);
|
| +
|
| + bool expired() => !(alive);
|
| +
|
| + void kill() {
|
| + alive = false;
|
| + killedOn = frameStart;
|
| + }
|
| +
|
| + /** Fire primary weapon(s). */
|
| +
|
| + void firePrimary(List bulletList) {
|
| + var playedSound = false;
|
| + // attempt to fire the primary weapon(s)
|
| + // first ensure player is alive and the shield is not up
|
| + if (alive && (!isShieldActive() || fireWhenShield)) {
|
| + for (var w in primaryWeapons.keys) {
|
| + var b = primaryWeapons[w].fire();
|
| + if (b != null) {
|
| + for (var i=0; i<b.length; i++) {
|
| + bulletList.add(b[i]);
|
| + }
|
| + if (!playedSound) {
|
| + soundManager.play('laser');
|
| + playedSound = true;
|
| + }
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Fire secondary weapon.
|
| + * @param bulletList {Array} to add bullet to on success
|
| + */
|
| + void fireSecondary(List bulletList) {
|
| + // Attempt to fire the secondary weapon and generate bomb object if
|
| + // successful. First ensure player is alive and the shield is not up.
|
| + if (alive && (!isShieldActive() || fireWhenShield) && energy > BOMB_ENERGY){
|
| + // now test we did not fire too recently
|
| + if (frameStart - bombRecharge > BOMB_RECHARGE_MS) {
|
| + // ok, update last fired time and we can now generate a bomb
|
| + bombRecharge = frameStart;
|
| +
|
| + // decrement energy supply
|
| + energy -= BOMB_ENERGY;
|
| +
|
| + // generate a vector rotated to the player heading and then add the
|
| + // current player vector to give the bomb the correct directional
|
| + // momentum.
|
| + var t = new Vector(0.0, -3.0);
|
| + t.rotate(heading * RAD);
|
| + t.add(velocity);
|
| +
|
| + bulletList.add(new Bomb(position.clone(), t));
|
| + }
|
| + }
|
| + }
|
| +
|
| + void onUpdate(_) {
|
| + // slowly recharge the shield - if not active
|
| + if (!isShieldActive() && energy < ENERGY_INIT) {
|
| + energy += 0.1;
|
| + }
|
| + }
|
| +
|
| + void reset(bool persistPowerUps) {
|
| + // reset energy, alive status, weapons and power up flags
|
| + alive = true;
|
| + if (!persistPowerUps) {
|
| + primaryWeapons = {};
|
| + primaryWeapons["main"] = new PrimaryWeapon(this);
|
| + fireWhenShield = false;
|
| + }
|
| + energy = ENERGY_INIT + SHIELD_MIN_PULSE; // for shield as below
|
| +
|
| + // active shield briefly
|
| + activateShield();
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Image Preloader class. Executes the supplied callback function once all
|
| + * registered images are loaded by the browser.
|
| + */
|
| +class Preloader {
|
| + Preloader() {
|
| + images = new List();
|
| + }
|
| +
|
| + /**
|
| + * Image list
|
| + *
|
| + * @property images
|
| + * @type Array
|
| + */
|
| + var images = [];
|
| +
|
| + /**
|
| + * Callback function
|
| + *
|
| + * @property callback
|
| + * @type Function
|
| + */
|
| + var callback = null;
|
| +
|
| + /**
|
| + * Images loaded so far counter
|
| + */
|
| + var counter = 0;
|
| +
|
| + /**
|
| + * Add an image to the list of images to wait for
|
| + */
|
| + void addImage(ImageElement img, String url) {
|
| + var me = this;
|
| + img.src = url;
|
| + // attach closure to the image onload handler
|
| + img.onLoad.listen((_) {
|
| + me.counter++;
|
| + if (me.counter == me.images.length) {
|
| + // all images are loaded - execute callback function
|
| + me.callback();
|
| + }
|
| + });
|
| + images.add(img);
|
| + }
|
| +
|
| + /**
|
| + * Load the images and call the supplied function when ready
|
| + */
|
| + void onLoadCallback(Function fn) {
|
| + counter = 0;
|
| + callback = fn;
|
| + // load the images
|
| + //for (var i=0, j = images.length; i<j; i++) {
|
| + // images[i].src = images[i].url;
|
| + //}
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Game prerenderer class.
|
| + */
|
| +class GamePrerenderer {
|
| + GamePrerenderer();
|
| +
|
| + /**
|
| + * Image list. Keyed by renderer ID - returning an array also. So to get
|
| + * the first image output by prerenderer with id "default":
|
| + * images["default"][0]
|
| + */
|
| + Map images = {};
|
| + Map _renderers = {};
|
| +
|
| + /** Add a renderer function to the list of renderers to execute. */
|
| + addRenderer(Function fn, String id) => _renderers[id] = fn;
|
| +
|
| +
|
| + /** Execute all prerender functions. */
|
| + void execute() {
|
| + var buffer = new CanvasElement();
|
| + for (var id in _renderers.keys) {
|
| + images[id] = _renderers[id](buffer);
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Asteroids prerenderer class.
|
| + *
|
| + * Encapsulates the early rendering of various effects used in the game. Each
|
| + * effect is rendered once to a hidden canvas object, the image data is
|
| + * extracted and stored in an Image object - which can then be reused later.
|
| + * This is much faster than rendering each effect again and again at runtime.
|
| + *
|
| + * The downside to this is that some constants are duplicated here and in the
|
| + * original classes - so updates to the original classes such as the weapon
|
| + * effects must be duplicated here.
|
| + */
|
| +class Prerenderer extends GamePrerenderer {
|
| + Prerenderer() : super() {
|
| +
|
| + // function to generate a set of point particle images
|
| + var fnPointRenderer = (CanvasElement buffer, String color) {
|
| + var imgs = [];
|
| + for (var size = 3; size <= 6; size++) {
|
| + var width = size << 1;
|
| + buffer.width = buffer.height = width;
|
| + CanvasRenderingContext2D ctx = buffer.getContext('2d');
|
| + var radgrad = ctx.createRadialGradient(size, size, size >> 1,
|
| + size, size, size);
|
| + radgrad.addColorStop(0, color);
|
| + radgrad.addColorStop(1, "#000");
|
| + ctx.fillStyle = radgrad;
|
| + ctx.fillRect(0, 0, width, width);
|
| + var img = new ImageElement();
|
| + img.src = buffer.toDataUrl("image/png");
|
| + imgs.add(img);
|
| + }
|
| + return imgs;
|
| + };
|
| +
|
| + // add the various point particle image prerenderers based on above function
|
| + // default explosion color
|
| + addRenderer((CanvasElement buffer) {
|
| + return fnPointRenderer(buffer, Colors.PARTICLE);
|
| + }, "points_${Colors.PARTICLE}");
|
| +
|
| + // player bullet impact particles
|
| + addRenderer((CanvasElement buffer) {
|
| + return fnPointRenderer(buffer, Colors.GREEN_LASER);
|
| + }, "points_${Colors.GREEN_LASER}");
|
| +
|
| + // enemy bullet impact particles
|
| + addRenderer((CanvasElement buffer) {
|
| + return fnPointRenderer(buffer, Colors.ENEMY_SHIP);
|
| + }, "points_${Colors.ENEMY_SHIP}");
|
| +
|
| + // add the smudge explosion particle image prerenderer
|
| + var fnSmudgeRenderer = (CanvasElement buffer, String color) {
|
| + var imgs = [];
|
| + for (var size = 4; size <= 32; size += 4) {
|
| + var width = size << 1;
|
| + buffer.width = buffer.height = width;
|
| + CanvasRenderingContext2D ctx = buffer.getContext('2d');
|
| + var radgrad = ctx.createRadialGradient(size, size, size >> 3,
|
| + size, size, size);
|
| + radgrad.addColorStop(0, color);
|
| + radgrad.addColorStop(1, "#000");
|
| + ctx.fillStyle = radgrad;
|
| + ctx.fillRect(0, 0, width, width);
|
| + var img = new ImageElement();
|
| + img.src = buffer.toDataUrl("image/png");
|
| + imgs.add(img);
|
| + }
|
| + return imgs;
|
| + };
|
| +
|
| + addRenderer((CanvasElement buffer) {
|
| + return fnSmudgeRenderer(buffer, Colors.PARTICLE);
|
| + }, "smudges_${Colors.PARTICLE}");
|
| +
|
| + addRenderer((CanvasElement buffer) {
|
| + return fnSmudgeRenderer(buffer, Colors.ENEMY_SHIP);
|
| + }, "smudges_${Colors.ENEMY_SHIP}");
|
| +
|
| + // standard player bullet
|
| + addRenderer((CanvasElement buffer) {
|
| + // NOTE: keep in sync with Asteroids.Bullet
|
| + var BULLET_WIDTH = 2, BULLET_HEIGHT = 6;
|
| + var imgs = [];
|
| + buffer.width = BULLET_WIDTH + GLOWSHADOWBLUR*2;
|
| + buffer.height = BULLET_HEIGHT + GLOWSHADOWBLUR*2;
|
| + CanvasRenderingContext2D ctx = buffer.getContext('2d');
|
| +
|
| + var rf = (width, height) {
|
| + ctx.beginPath();
|
| + ctx.moveTo(0, height);
|
| + ctx.lineTo(width, 0);
|
| + ctx.lineTo(0, -height);
|
| + ctx.lineTo(-width, 0);
|
| + ctx.closePath();
|
| + };
|
| +
|
| + ctx.shadowBlur = GLOWSHADOWBLUR;
|
| + ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
|
| + ctx.shadowColor = ctx.fillStyle = Colors.GREEN_LASER_DARK;
|
| + rf(BULLET_WIDTH-1, BULLET_HEIGHT-1);
|
| + ctx.fill();
|
| + ctx.shadowColor = ctx.fillStyle = Colors.GREEN_LASER;
|
| + rf(BULLET_WIDTH, BULLET_HEIGHT);
|
| + ctx.fill();
|
| + var img = new ImageElement();
|
| + img.src = buffer.toDataUrl("image/png");
|
| + return img;
|
| + }, "bullet");
|
| +
|
| + // player bullet X2
|
| + addRenderer((CanvasElement buffer) {
|
| + // NOTE: keep in sync with Asteroids.BulletX2
|
| + var BULLET_WIDTH = 2, BULLET_HEIGHT = 6;
|
| + buffer.width = BULLET_WIDTH + GLOWSHADOWBLUR*4;
|
| + buffer.height = BULLET_HEIGHT + GLOWSHADOWBLUR*2;
|
| + CanvasRenderingContext2D ctx = buffer.getContext('2d');
|
| +
|
| + var rf = (width, height) {
|
| + ctx.beginPath();
|
| + ctx.moveTo(0, height);
|
| + ctx.lineTo(width, 0);
|
| + ctx.lineTo(0, -height);
|
| + ctx.lineTo(-width, 0);
|
| + ctx.closePath();
|
| + };
|
| +
|
| + ctx.shadowBlur = GLOWSHADOWBLUR;
|
| + ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
|
| + ctx.save();
|
| + ctx.translate(-4, 0);
|
| + ctx.shadowColor = ctx.fillStyle = Colors.GREEN_LASERX2_DARK;
|
| + rf(BULLET_WIDTH-1, BULLET_HEIGHT-1);
|
| + ctx.fill();
|
| + ctx.shadowColor = ctx.fillStyle = Colors.GREEN_LASERX2;
|
| + rf(BULLET_WIDTH, BULLET_HEIGHT);
|
| + ctx.fill();
|
| + ctx.translate(8, 0);
|
| + ctx.shadowColor = ctx.fillStyle = Colors.GREEN_LASERX2_DARK;
|
| + rf(BULLET_WIDTH-1, BULLET_HEIGHT-1);
|
| + ctx.fill();
|
| + ctx.shadowColor = ctx.fillStyle = Colors.GREEN_LASERX2;
|
| + rf(BULLET_WIDTH, BULLET_HEIGHT);
|
| + ctx.fill();
|
| + ctx.restore();
|
| + var img = new ImageElement();
|
| + img.src = buffer.toDataUrl("image/png");
|
| + return img;
|
| + }, "bulletx2");
|
| +
|
| + // player bomb weapon
|
| + addRenderer((CanvasElement buffer) {
|
| + // NOTE: keep in sync with Asteroids.Bomb
|
| + var BOMB_RADIUS = 4;
|
| + buffer.width = buffer.height = BOMB_RADIUS*2 + GLOWSHADOWBLUR*2;
|
| + CanvasRenderingContext2D ctx = buffer.getContext('2d');
|
| +
|
| + var rf = () {
|
| + ctx.beginPath();
|
| + ctx.moveTo(BOMB_RADIUS * 2, 0);
|
| + for (var i = 0; i < 15; i++) {
|
| + ctx.rotate(PIO8);
|
| + if (i % 2 == 0) {
|
| + ctx.lineTo((BOMB_RADIUS * 2 / 0.525731) * 0.200811, 0);
|
| + } else {
|
| + ctx.lineTo(BOMB_RADIUS * 2, 0);
|
| + }
|
| + }
|
| + ctx.closePath();
|
| + };
|
| +
|
| + ctx.shadowBlur = GLOWSHADOWBLUR;
|
| + ctx.shadowColor = ctx.fillStyle = Colors.PLAYER_BOMB;
|
| + ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
|
| + rf();
|
| + ctx.fill();
|
| +
|
| + var img = new ImageElement();
|
| + img.src = buffer.toDataUrl("image/png");
|
| + return img;
|
| + }, "bomb");
|
| +
|
| + //enemy weapon
|
| + addRenderer((CanvasElement buffer) {
|
| + // NOTE: keep in sync with Asteroids.EnemyBullet
|
| + var BULLET_RADIUS = 4;
|
| + var imgs = [];
|
| + buffer.width = buffer.height = BULLET_RADIUS*2 + GLOWSHADOWBLUR*2;
|
| + CanvasRenderingContext2D ctx = buffer.getContext('2d');
|
| +
|
| + var rf = () {
|
| + ctx.beginPath();
|
| + ctx.moveTo(BULLET_RADIUS * 2, 0);
|
| + for (var i=0; i<7; i++) {
|
| + ctx.rotate(PIO4);
|
| + if (i % 2 == 0) {
|
| + ctx.lineTo((BULLET_RADIUS * 2/0.525731) * 0.200811, 0);
|
| + } else {
|
| + ctx.lineTo(BULLET_RADIUS * 2, 0);
|
| + }
|
| + }
|
| + ctx.closePath();
|
| + };
|
| +
|
| + ctx.shadowBlur = GLOWSHADOWBLUR;
|
| + ctx.shadowColor = ctx.fillStyle = Colors.ENEMY_SHIP;
|
| + ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
|
| + ctx.beginPath();
|
| + ctx.arc(0, 0, BULLET_RADIUS-1, 0, TWOPI, true);
|
| + ctx.closePath();
|
| + ctx.fill();
|
| + rf();
|
| + ctx.fill();
|
| +
|
| + var img = new ImageElement();
|
| + img.src = buffer.toDataUrl("image/png");
|
| + return img;
|
| + }, "enemybullet");
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Game scene base class.
|
| + */
|
| +class Scene {
|
| + bool playable;
|
| + Interval interval;
|
| +
|
| + Scene([this.playable = true, this.interval = null]);
|
| +
|
| + /** Return true if this scene should update the actor list. */
|
| + bool isPlayable() => playable;
|
| +
|
| + void onInitScene() {
|
| + if (interval != null) {
|
| + // reset interval flag
|
| + interval.reset();
|
| + }
|
| + }
|
| +
|
| + void onBeforeRenderScene() {}
|
| + void onRenderScene(ctx) {}
|
| + void onRenderInterval(ctx) {}
|
| + void onMouseDownHandler(e) {}
|
| + void onMouseUpHandler(e) {}
|
| + void onKeyDownHandler(int keyCode) {}
|
| + void onKeyUpHandler(int keyCode) {}
|
| + bool isComplete() => false;
|
| +
|
| + bool onAccelerometer(double x, double y, double z) {
|
| + return true;
|
| + }
|
| +}
|
| +
|
| +class SoundManager {
|
| + bool _isDesktopEmulator;
|
| + Map _sounds = {};
|
| +
|
| + SoundManager(this._isDesktopEmulator);
|
| +
|
| + void createSound(Map props) {
|
| + if (!_isDesktopEmulator) {
|
| + var a = new AudioElement();
|
| + a.volume = props['volume'] / 100.0;;
|
| + a.src = props['url'];
|
| + _sounds[props['id']] = a;
|
| + }
|
| + }
|
| +
|
| + void play(String id) {
|
| + if (!_isDesktopEmulator) {
|
| + _sounds[id].play();
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * An actor that can be rendered by a bitmap. The sprite handling code deals
|
| + * with the increment of the current frame within the supplied bitmap sprite
|
| + * strip image, based on animation direction, animation speed and the animation
|
| + * length before looping. Call renderSprite() each frame.
|
| + *
|
| + * NOTE: by default sprites source images are 64px wide 64px by N frames high
|
| + * and scaled to the appropriate final size. Any other size input source should
|
| + * be set in the constructor.
|
| + */
|
| +class SpriteActor extends Actor {
|
| + SpriteActor(Vector position, Vector vector, [this.frameSize = 64])
|
| + : super(position, vector);
|
| +
|
| + /** Size in pixels of the width/height of an individual frame in the image. */
|
| + int frameSize;
|
| +
|
| + /**
|
| + * Animation image sprite reference.
|
| + * Sprite image sources are all currently 64px wide 64px by N frames high.
|
| + */
|
| + ImageElement animImage = null;
|
| +
|
| + /** Length in frames of the sprite animation. */
|
| + int animLength = 0;
|
| +
|
| + /** Animation direction, true for forward, false for reverse. */
|
| + bool animForward = true;
|
| +
|
| + /** Animation frame inc/dec speed. */
|
| + double animSpeed = 1.0;
|
| +
|
| + /** Current animation frame index. */
|
| + int animFrame = 0;
|
| +
|
| + /**
|
| + * Render sprite graphic based on current anim image, frame and anim direction
|
| + * Automatically updates the current anim frame.
|
| + */
|
| + void renderSprite(CanvasRenderingContext2D ctx, num x, num y, num s) {
|
| + renderImage(ctx, animImage, 0, animFrame << 6, frameSize, x, y, s);
|
| +
|
| + // update animation frame index
|
| + if (animForward) {
|
| + animFrame += (animSpeed * frameMultiplier).toInt();
|
| + if (animFrame >= animLength) {
|
| + animFrame = 0;
|
| + }
|
| + } else {
|
| + animFrame -= (animSpeed * frameMultiplier).toInt();
|
| + if (animFrame < 0) {
|
| + animFrame = animLength - 1;
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +class Star {
|
| + Star();
|
| +
|
| + double MAXZ = 12.0;
|
| + double VELOCITY = 0.85;
|
| +
|
| + num x = 0;
|
| + num y = 0;
|
| + num z = 0;
|
| + num prevx = 0;
|
| + num prevy = 0;
|
| +
|
| + void init() {
|
| + // select a random point for the initial location
|
| + prevx = prevy = 0;
|
| + x = (random() * GameHandler.width - (GameHandler.width * 0.5)) * MAXZ;
|
| + y = (random() * GameHandler.height - (GameHandler.height * 0.5)) * MAXZ;
|
| + z = MAXZ;
|
| + }
|
| +
|
| + void render(CanvasRenderingContext2D ctx) {
|
| + var xx = x / z;
|
| + var yy = y / z;
|
| +
|
| + if (prevx != 0) {
|
| + ctx.lineWidth = 1.0 / z * 5 + 1;
|
| + ctx.beginPath();
|
| + ctx.moveTo(prevx + (GameHandler.width * 0.5),
|
| + prevy + (GameHandler.height * 0.5));
|
| + ctx.lineTo(xx + (GameHandler.width * 0.5),
|
| + yy + (GameHandler.height * 0.5));
|
| + ctx.stroke();
|
| + }
|
| +
|
| + prevx = xx;
|
| + prevy = yy;
|
| + }
|
| +}
|
| +
|
| +void drawText(CanvasRenderingContext2D g,
|
| + String txt, String font, num x, num y,
|
| + [String color]) {
|
| + g.save();
|
| + if (color != null) g.strokeStyle = color;
|
| + g.font = font;
|
| + g.strokeText(txt, x, y);
|
| + g.restore();
|
| +}
|
| +
|
| +void centerDrawText(CanvasRenderingContext2D g, String txt, String font, num y,
|
| + [String color]) {
|
| + g.save();
|
| + if (color != null) g.strokeStyle = color;
|
| + g.font = font;
|
| + g.strokeText(txt, (GameHandler.width - g.measureText(txt).width) / 2, y);
|
| + g.restore();
|
| +}
|
| +
|
| +void fillText(CanvasRenderingContext2D g, String txt, String font, num x, num y,
|
| + [String color]) {
|
| + g.save();
|
| + if (color != null) g.fillStyle = color;
|
| + g.font = font;
|
| + g.fillText(txt, x, y);
|
| + g.restore();
|
| +}
|
| +
|
| +void centerFillText(CanvasRenderingContext2D g, String txt, String font, num y,
|
| + [String color]) {
|
| + g.save();
|
| + if (color != null) g.fillStyle = color;
|
| + g.font = font;
|
| + g.fillText(txt, (GameHandler.width - g.measureText(txt).width) / 2, y);
|
| + g.restore();
|
| +}
|
| +
|
| +void drawScaledImage(CanvasRenderingContext2D ctx, ImageElement image,
|
| + num nx, num ny, num ns, num x, num y, num s) {
|
| + ctx.drawImageToRect(image, new Rect(x, y, s, s),
|
| + sourceRect: new Rect(nx, ny, ns, ns));
|
| +}
|
| +/**
|
| + * This method will automatically correct for objects moving on/off
|
| + * a cyclic canvas play area - if so it will render the appropriate stencil
|
| + * sections of the sprite top/bottom/left/right as needed to complete the image.
|
| + * Note that this feature can only be used if the sprite is absolutely
|
| + * positioned and not translated/rotated into position by canvas operations.
|
| + */
|
| +void renderImage(CanvasRenderingContext2D ctx, ImageElement image,
|
| + num nx, num ny, num ns, num x, num y, num s) {
|
| + print("renderImage(_,$nx,$ny,$ns,$ns,$x,$y,$s,$s)");
|
| + ctx.drawImageScaledFromSource(image, nx, ny, ns, ns, x, y, s, s);
|
| +
|
| + if (x < 0) {
|
| + ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
|
| + GameHandler.width + x, y, s, s);
|
| + }
|
| + if (y < 0) {
|
| + ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
|
| + x, GameHandler.height + y, s, s);
|
| + }
|
| + if (x < 0 && y < 0) {
|
| + ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
|
| + GameHandler.width + x, GameHandler.height + y, s, s);
|
| + }
|
| + if (x + s > GameHandler.width) {
|
| + ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
|
| + x - GameHandler.width, y, s, s);
|
| + }
|
| + if (y + s > GameHandler.height) {
|
| + ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
|
| + x, y - GameHandler.height, s, s);
|
| + }
|
| + if (x + s > GameHandler.width && y + s > GameHandler.height) {
|
| + ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
|
| + x - GameHandler.width, y - GameHandler.height, s, s);
|
| + }
|
| +}
|
| +
|
| +void renderImageRotated(CanvasRenderingContext2D ctx, ImageElement image,
|
| + num x, num y, num w, num h, num r) {
|
| + var w2 = w*0.5, h2 = h*0.5;
|
| + var rf = (tx, ty) {
|
| + ctx.save();
|
| + ctx.translate(tx, ty);
|
| + ctx.rotate(r);
|
| + ctx.drawImage(image, -w2, -h2);
|
| + ctx.restore();
|
| + };
|
| +
|
| + rf(x, y);
|
| +
|
| + if (x - w2 < 0) {
|
| + rf(GameHandler.width + x, y);
|
| + }
|
| + if (y - h2 < 0) {
|
| + rf(x, GameHandler.height + y);
|
| + }
|
| + if (x - w2 < 0 && y - h2 < 0) {
|
| + rf(GameHandler.width + x, GameHandler.height + y);
|
| + }
|
| + if (x - w2 + w > GameHandler.width) {
|
| + rf(x - GameHandler.width, y);
|
| + }
|
| + if (y - h2 + h > GameHandler.height){
|
| + rf(x, y - GameHandler.height);
|
| + }
|
| + if (x - w2 + w > GameHandler.width && y - h2 + h > GameHandler.height) {
|
| + rf(x - GameHandler.width, y - GameHandler.height);
|
| + }
|
| +}
|
| +
|
| +void renderImageRotated2(CanvasRenderingContext2D ctx, ImageElement image,
|
| + num x, num y, num w, num h, num r) {
|
| + print("Rendering rotated sprite ${image.src} to dest $x,$y");
|
| + var w2 = w*0.5, h2 = h*0.5;
|
| + var rf = (tx, ty) {
|
| + ctx.save();
|
| + ctx.translate(tx, ty);
|
| + ctx.rotate(r);
|
| + ctx.drawImage(image, -w2, -h2);
|
| + ctx.restore();
|
| + };
|
| +
|
| + rf(x, y);
|
| +
|
| + if (x - w2 < 0) {
|
| + rf(GameHandler.width + x, y);
|
| + }
|
| + if (y - h2 < 0) {
|
| + rf(x, GameHandler.height + y);
|
| + }
|
| + if (x - w2 < 0 && y - h2 < 0) {
|
| + rf(GameHandler.width + x, GameHandler.height + y);
|
| + }
|
| + if (x - w2 + w > GameHandler.width) {
|
| + rf(x - GameHandler.width, y);
|
| + }
|
| + if (y - h2 + h > GameHandler.height){
|
| + rf(x, y - GameHandler.height);
|
| + }
|
| + if (x - w2 + w > GameHandler.width && y - h2 + h > GameHandler.height) {
|
| + rf(x - GameHandler.width, y - GameHandler.height);
|
| + }
|
| +}
|
| +
|
| +class Vector {
|
| + num x, y;
|
| +
|
| + Vector(this.x, this.y);
|
| +
|
| + Vector clone() => new Vector(x, y);
|
| +
|
| + void set(Vector v) {
|
| + x = v.x;
|
| + y = v.y;
|
| + }
|
| +
|
| + Vector add(Vector v) {
|
| + x += v.x;
|
| + y += v.y;
|
| + return this;
|
| + }
|
| +
|
| + Vector nadd(Vector v) => new Vector(x + v.x, y + v.y);
|
| +
|
| + Vector sub(Vector v) {
|
| + x -= v.x;
|
| + y -= v.y;
|
| + return this;
|
| + }
|
| +
|
| + Vector nsub(Vector v) => new Vector(x - v.x, y - v.y);
|
| +
|
| + double dot(Vector v) => x * v.x + y * v.y;
|
| +
|
| + double length() => Math.sqrt(x * x + y * y);
|
| +
|
| + double distance(Vector v) {
|
| + var dx = x - v.x;
|
| + var dy = y - v.y;
|
| + return Math.sqrt(dx * dx + dy * dy);
|
| + }
|
| +
|
| + double theta() => Math.atan2(y, x);
|
| +
|
| + double thetaTo(Vector vec) {
|
| + // calc angle between the two vectors
|
| + var v = clone().norm();
|
| + var w = vec.clone().norm();
|
| + return Math.sqrt(v.dot(w));
|
| + }
|
| +
|
| + double thetaTo2(Vector vec) =>
|
| + Math.atan2(vec.y, vec.x) - Math.atan2(y, x);
|
| +
|
| + Vector norm() {
|
| + var len = length();
|
| + x /= len;
|
| + y /= len;
|
| + return this;
|
| + }
|
| +
|
| + Vector nnorm() {
|
| + var len = length();
|
| + return new Vector(x / len, y / len);
|
| + }
|
| +
|
| + rotate(num a) {
|
| + var ca = Math.cos(a);
|
| + var sa = Math.sin(a);
|
| + var newx = x*ca - y*sa;
|
| + var newy = x*sa + y*ca;
|
| + x = newx;
|
| + y = newy;
|
| + return this;
|
| + }
|
| +
|
| + Vector nrotate(num a) {
|
| + var ca = Math.cos(a);
|
| + var sa = Math.sin(a);
|
| + return new Vector(x * ca - y * sa, x * sa + y * ca);
|
| + }
|
| +
|
| + Vector invert() {
|
| + x = -x;
|
| + y = -y;
|
| + return this;
|
| + }
|
| +
|
| + Vector ninvert() {
|
| + return new Vector(-x, -y);
|
| + }
|
| +
|
| + Vector scale(num s) {
|
| + x *= s;
|
| + y *= s;
|
| + return this;
|
| + }
|
| +
|
| + Vector nscale(num s) {
|
| + return new Vector(x * s, y * s);
|
| + }
|
| +
|
| + Vector scaleTo(num s) {
|
| + var len = s / length();
|
| + x *= len;
|
| + y *= len;
|
| + return this;
|
| + }
|
| +
|
| + nscaleTo(num s) {
|
| + var len = s / length();
|
| + return new Vector(x * len, y * len);
|
| + }
|
| +
|
| + trim(num minx, num maxx, num miny, num maxy) {
|
| + if (x < minx) x = minx;
|
| + else if (x > maxx) x = maxx;
|
| + if (y < miny) y = miny;
|
| + else if (y > maxy) y = maxy;
|
| + }
|
| +
|
| + wrap(num minx, num maxx, num miny, num maxy) {
|
| + if (x < minx) x = maxx;
|
| + else if (x > maxx) x = minx;
|
| + if (y < miny) y = maxy;
|
| + else if (y > maxy) y = miny;
|
| + }
|
| +
|
| + String toString() => "<$x, $y>";
|
| +}
|
| +
|
| +class Weapon {
|
| + Weapon(this.player, [this.rechargeTime = 125]);
|
| +
|
| + int rechargeTime;
|
| + int lastFired = 0;
|
| + Player player;
|
| +
|
| + bool canFire() =>
|
| + (GameHandler.frameStart - lastFired) >= rechargeTime;
|
| +
|
| + List fire() {
|
| + if (canFire()) {
|
| + lastFired = GameHandler.frameStart;
|
| + return doFire();
|
| + }
|
| + }
|
| +
|
| + Bullet makeBullet(double headingDelta, double vectorY,
|
| + [int lifespan = 1300]) {
|
| + var h = player.heading - headingDelta;
|
| + var t = new Vector(0.0, vectorY).rotate(h * RAD).add(player.velocity);
|
| + return new Bullet(player.position.clone(), t, h, lifespan);
|
| + }
|
| +
|
| + List doFire() => [];
|
| +}
|
| +
|
| +class PrimaryWeapon extends Weapon {
|
| + PrimaryWeapon(Player player) : super(player);
|
| +
|
| + List doFire() => [ makeBullet(0.0, -4.5) ];
|
| +}
|
| +
|
| +class TwinCannonsWeapon extends Weapon {
|
| + TwinCannonsWeapon(Player player) : super(player, 150);
|
| +
|
| + List doFire() {
|
| + var h = player.heading;
|
| + var t = new Vector(0.0, -4.5).rotate(h * RAD).add(player.velocity);
|
| + return [ new BulletX2(player.position.clone(), t, h) ];
|
| + }
|
| +}
|
| +
|
| +class VSprayCannonsWeapon extends Weapon {
|
| + VSprayCannonsWeapon(Player player) : super(player, 250);
|
| +
|
| + List doFire() =>
|
| + [ makeBullet(-15.0, -3.75),
|
| + makeBullet(0.0, -3.75),
|
| + makeBullet(15.0, -3.75) ];
|
| +}
|
| +
|
| +class SideGunWeapon extends Weapon {
|
| + SideGunWeapon(Player player) : super(player, 250);
|
| +
|
| + List doFire() =>
|
| + [ makeBullet(-90.0, -4.5, 750),
|
| + makeBullet(+90.0, -4.5, 750)];
|
| +}
|
| +
|
| +class RearGunWeapon extends Weapon {
|
| + RearGunWeapon(Player player) : super(player, 250);
|
| +
|
| + List doFire() => [makeBullet(180.0, -4.5, 750)];
|
| +}
|
| +
|
| +class Input {
|
| + bool left, right, thrust, shield, fireA, fireB;
|
| +
|
| + Input() { reset(); }
|
| +
|
| + void reset() {
|
| + left = right = thrust = shield = fireA = fireB = false;
|
| + }
|
| +}
|
| +
|
| +void resize(int w, int h) {}
|
| +
|
| +
|
| +void setup(canvasp, int w, int h, int f) {
|
| + var canvas;
|
| + if (canvasp == null) {
|
| + log("Allocating canvas");
|
| + canvas = new CanvasElement(width: w, height: h);
|
| + document.body.nodes.add(canvas);
|
| + } else {
|
| + log("Using parent canvas");
|
| + canvas = canvasp;
|
| + }
|
| +
|
| + for (var i = 0; i < 4; i++) {
|
| + _asteroidImgs.add(new ImageElement());
|
| + }
|
| + // attach to the image onload handler
|
| + // once the background is loaded, we can boot up the game
|
| + _backgroundImg.onLoad.listen((e) {
|
| + // init our game with Game.Main derived instance
|
| + log("Loaded background image ${_backgroundImg.src}");
|
| + GameHandler.init(canvas);
|
| + GameHandler.start(new AsteroidsMain());
|
| + });
|
| + _backgroundImg.src = 'bg3_1.png';
|
| + loadSounds(f == 1);
|
| +}
|
| +
|
| +void loadSounds(bool isDesktopEmulator) {
|
| + soundManager = new SoundManager(isDesktopEmulator);
|
| + // load game sounds
|
| + soundManager.createSound({
|
| + 'id': 'laser',
|
| + 'url': 'laser.$sfx_extension',
|
| + 'volume': 40,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| + soundManager.createSound({
|
| + 'id': 'enemy_bomb',
|
| + 'url': 'enemybomb.$sfx_extension',
|
| + 'volume': 60,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| + soundManager.createSound({
|
| + 'id': 'big_boom',
|
| + 'url': 'bigboom.$sfx_extension',
|
| + 'volume': 50,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| + soundManager.createSound({
|
| + 'id': 'asteroid_boom1',
|
| + 'url': 'explosion1.$sfx_extension',
|
| + 'volume': 50,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| + soundManager.createSound({
|
| + 'id': 'asteroid_boom2',
|
| + 'url': 'explosion2.$sfx_extension',
|
| + 'volume': 50,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| + soundManager.createSound({
|
| + 'id': 'asteroid_boom3',
|
| + 'url': 'explosion3.$sfx_extension',
|
| + 'volume': 50,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| + soundManager.createSound({
|
| + 'id': 'asteroid_boom4',
|
| + 'url': 'explosion4.$sfx_extension',
|
| + 'volume': 50,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| + soundManager.createSound({
|
| + 'id': 'powerup',
|
| + 'url': 'powerup.$sfx_extension',
|
| + 'volume': 50,
|
| + 'autoLoad': true,
|
| + 'multiShot': true
|
| + });
|
| +}
|
| +
|
|
|