Chromium Code Reviews| Index: samples/openglui/src/blasteroids.dart |
| =================================================================== |
| --- samples/openglui/src/blasteroids.dart (revision 0) |
| +++ samples/openglui/src/blasteroids.dart (revision 0) |
| @@ -0,0 +1,3341 @@ |
| +// 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; |
|
vsm
2013/04/01 14:00:17
Consider breaking this file up - perhaps one file
|
| + |
| +import 'dart:math' as Math; |
| +import 'gl.dart'; |
| + |
| +const RAD = Math.PI/180.0; |
|
vsm
2013/04/01 14:00:17
Nit: spaces around arithmetic ops.
|
| +const PI = Math.PI; |
| +const TWOPI = Math.PI*2; |
| +const ONEOPI = 1.0 / Math.PI; |
| +const PIO2 = Math.PI/2; |
| +const PIO4 = Math.PI/4; |
| +const PIO8 = Math.PI/8; |
| +const PIO16 = Math.PI/16; |
| +const PIO32 = Math.PI/32; |
| + |
| +var _rnd = new Math.Random(); |
| +double random() => _rnd.nextDouble(); |
| +int randomInt(int min, int max) => min + _rnd.nextInt(max - min + 1); |
| + |
| +class KEY { |
|
vsm
2013/04/01 14:00:17
KEY -> Key per style?
|
| + static const int SHIFT = 16; |
| + static const int CTRL = 17; |
| + static const int ESC = 27; |
| + static const int RIGHT = 39; |
| + static const int UP = 38; |
| + static const int LEFT = 37; |
| + static const int DOWN = 40; |
| + static const int SPACE = 32; |
| + static const int A = 65; |
| + static const int E = 69; |
| + static const int G = 71; |
| + static const int L = 76; |
| + static const int P = 80; |
| + static const int R = 82; |
| + static const int S = 83; |
| + static const int Z = 90; |
| +} |
| + |
| +// Globals |
| +var DEBUG = { |
|
vsm
2013/04/01 14:00:17
const DEBUG or var debug
|
| + 'enabled': false, |
| + 'invincible': false, |
| + 'collisionRadius': false, |
| + 'fps': true |
| +}; |
| + |
| +var GLOWEFFECT = true; |
|
vsm
2013/04/01 14:00:17
const or lowercase for these 3
|
| +var GLOWSHADOWBLUR = 8; |
| +var SCOREDBKEY = "asteroids-score-1.1"; |
| + |
| +var g_asteroidImgs = []; |
|
vsm
2013/04/01 14:00:17
'g_' -> '_' ?
|
| +var g_shieldImg = new ImageElement(); |
| +var g_backgroundImg = new ImageElement(); |
| +var g_playerImg = new ImageElement(); |
| +var g_enemyshipImg = new ImageElement(); |
| +var soundManager; |
| + |
| +/** Asteroids colour constants */ |
| +class Colours { |
|
vsm
2013/04/01 14:00:17
We've been using American spelling (Color) in the
|
| + 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; |
| + int SCENE_LENGTH = 400; |
| + int SCENE_FADE = 75; |
|
vsm
2013/04/01 14:00:17
const or lowercase on these two.
|
| + 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 colour; |
|
vsm
2013/04/01 14:00:17
colour -> color
|
| + |
| + TextIndicator(Vector position, Vector velocity, this.msg, |
| + [this.textSize = 12, this.colour = "white", |
| + int fl = 500]) : |
| + super(position, velocity, fl), fadeLength = fl; |
| + |
| + const int 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, colour); |
| + ctx.restore(); |
| + } |
| +} |
| + |
| +/** Score indicator effect actor class. */ |
| +class ScoreIndicator extends TextIndicator { |
| + ScoreIndicator(Vector position, Vector velocity, int score, |
| + [int textSize = 12, String prefix = '', String colour = "white", |
| + int fadeLength = 500]) : |
| + super(position, velocity, '${prefix.length > 0 ? "$prefix " : ""}${score}', |
| + textSize, colour, fadeLength); |
| +} |
| + |
| +/** Power up collectable. */ |
| +class PowerUp extends EffectActor { |
| + PowerUp(Vector position, Vector velocity) |
| + : super(position, velocity); |
| + |
| + const int 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) { |
| + // Rrandomly select a powerup to apply. |
|
vsm
2013/04/01 14:00:17
Rrandomly -> Randomly
|
| + 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; |
| + |
| + // Aadd a BIG explosion actor at the smart bomb weapon position and vector |
|
vsm
2013/04/01 14:00:17
line len
|
| + 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 = g_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 int 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. |
| + //print("Position ${position}, Vector ${vector}"); |
| + //print("Render at ${position.x - rad - 2}, ${position.y - rad - 2}"); |
|
vsm
2013/04/01 14:00:17
Delete commented code or put under debug flag.
|
| + 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 = g_enemyshipImg; |
| + animLength = SHIP_ANIM_LENGTH; |
| + } |
| + |
| + const int 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 (localStorage) { |
|
vsm
2013/04/01 14:00:17
Delete commented code or add TODO.
|
| + localStorage.setItem(SCOREDBKEY, this.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 double FPSMS = 1000/60; |
|
vsm
2013/04/01 14:00:17
You can omit the "double". Spacing around '/'.
|
| + |
| + 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)"); |
|
vsm
2013/04/01 14:00:17
INit -> Init
|
| + } |
| + |
| + /** |
| + * 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(g_playerImg, 'player.png'); |
| + loader.addImage(g_asteroidImgs[0], 'asteroid1.png'); |
| + loader.addImage(g_asteroidImgs[1], 'asteroid2.png'); |
| + loader.addImage(g_asteroidImgs[2], 'asteroid3.png'); |
| + loader.addImage(g_asteroidImgs[3], 'asteroid4.png'); |
| + loader.addImage(g_shieldImg, 'shield.png'); |
| + loader.addImage(g_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(g_backgroundImg, destRect, |
| + // sourceRect:sourceRect); |
| + ctx.drawImageScaledFromSource(g_backgroundImg, |
| + backgroundX, 0, w, h, 0, 0, w, h); |
| + |
| + backgroundX += (GameHandler.frameMultiplier / 4.0); |
| + if (backgroundX >= g_backgroundImg.width / 2) { |
| + backgroundX -= g_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']) { |
| + GLOWEFFECT = !GLOWEFFECT; |
| + 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, g_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 int BULLET_WIDTH = 2; |
| + const int BULLET_HEIGHT = 6; |
| + const int 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 double BOMB_RADIUS = 4.0; |
| + const int FADE_LENGTH = 200; |
| + const int 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 double BULLET_RADIUS = 4.0; |
| + const int FADE_LENGTH = 200; |
|
vsm
2013/04/01 14:00:17
You can drop double and int above - obvious from d
|
| + |
| + 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 colour; |
|
vsm
2013/04/01 14:00:17
color. :-)
|
| + double rotate; |
| + double rotationv; |
| + |
| + Particle(Vector position, Vector velocity, this.size, this.type, |
| + int lifespan, this.fadelength, |
| + [this.colour = Colours.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_${colour}"][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 = colour; |
| + 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_${colour}"][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, Colours.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, Colours.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 int FADE_LENGTH = 300; |
|
vsm
2013/04/01 14:00:17
drop int
|
| + |
| + 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, Colours.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, Colours.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 = g_shieldImg; |
| + animLength = SHIELD_ANIM_LENGTH; |
| + |
| + // setup weapons |
| + primaryWeapons = {}; |
| + } |
| + |
| + double MAX_PLAYER_VELOCITY = 8.0; |
|
vsm
2013/04/01 14:00:17
Mark all these const or lowercase.
|
| + num PLAYER_RADIUS = 9; |
| + num SHIELD_RADIUS = 14; |
| + num SHIELD_ANIM_LENGTH = 100; |
| + num SHIELD_MIN_PULSE = 20; |
| + num ENERGY_INIT = 400; |
| + num THRUST_DELAY_MS = 100; |
| + num BOMB_RECHARGE_MS = 800; |
| + num 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 = Colours.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, g_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 colour) { |
| + 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, colour); |
| + 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 colour |
| + addRenderer((CanvasElement buffer) { |
| + return fnPointRenderer(buffer, Colours.PARTICLE); |
| + }, "points_${Colours.PARTICLE}"); |
| + |
| + // player bullet impact particles |
| + addRenderer((CanvasElement buffer) { |
| + return fnPointRenderer(buffer, Colours.GREEN_LASER); |
| + }, "points_${Colours.GREEN_LASER}"); |
| + |
| + // enemy bullet impact particles |
| + addRenderer((CanvasElement buffer) { |
| + return fnPointRenderer(buffer, Colours.ENEMY_SHIP); |
| + }, "points_${Colours.ENEMY_SHIP}"); |
| + |
| + // add the smudge explosion particle image prerenderer |
| + var fnSmudgeRenderer = (CanvasElement buffer, String colour) { |
| + 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, colour); |
| + 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, Colours.PARTICLE); |
| + }, "smudges_${Colours.PARTICLE}"); |
| + |
| + addRenderer((CanvasElement buffer) { |
| + return fnSmudgeRenderer(buffer, Colours.ENEMY_SHIP); |
| + }, "smudges_${Colours.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 = Colours.GREEN_LASER_DARK; |
| + rf(BULLET_WIDTH-1, BULLET_HEIGHT-1); |
| + ctx.fill(); |
| + ctx.shadowColor = ctx.fillStyle = Colours.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 = Colours.GREEN_LASERX2_DARK; |
| + rf(BULLET_WIDTH-1, BULLET_HEIGHT-1); |
| + ctx.fill(); |
| + ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASERX2; |
| + rf(BULLET_WIDTH, BULLET_HEIGHT); |
| + ctx.fill(); |
| + ctx.translate(8, 0); |
| + ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASERX2_DARK; |
| + rf(BULLET_WIDTH-1, BULLET_HEIGHT-1); |
| + ctx.fill(); |
| + ctx.shadowColor = ctx.fillStyle = Colours.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 = Colours.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 = Colours.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 { |
| + |
| + Map sounds = {}; |
| + |
| + void createSound(Map props) { |
| + var a = new AudioElement(); |
| + a.volume = props['volume'] / 100.0;; |
| + a.src = props['url']; |
| + sounds[props['id']] = a; |
| + } |
| + |
| + void play(String id) { |
| + 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) { |
| + 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++) { |
| + g_asteroidImgs.add(new ImageElement()); |
| + } |
| + // attach to the image onload handler |
| + // once the background is loaded, we can boot up the game |
| + g_backgroundImg.onLoad.listen((e) { |
| + // init our game with Game.Main derived instance |
| + log("Loaded background image ${g_backgroundImg.src}"); |
| + GameHandler.init(canvas); |
| + GameHandler.start(new AsteroidsMain()); |
| + }); |
| + g_backgroundImg.src = 'bg3_1.png'; |
| + loadSounds(); |
| +} |
| + |
| +void loadSounds() { |
| + soundManager = new SoundManager(); |
| + // 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 |
| + }); |
| +} |
| + |
| + |
| + |
| + |
| + |