Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(118)

Unified Diff: samples/openglui/src/blasteroids.dart

Issue 13345002: Cleaned up OpenGLUI samples and added Blasteroids. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « samples/openglui/pubspec.yaml ('k') | samples/openglui/src/flashingbox.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
+ });
+}
+
« no previous file with comments | « samples/openglui/pubspec.yaml ('k') | samples/openglui/src/flashingbox.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698