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

Side by Side Diff: samples/openglui/src/blasteroids.dart

Issue 13345002: Cleaned up OpenGLUI samples and added Blasteroids. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // A Dart port of Kevin Roast's Asteroids game.
2 // http://www.kevs3d.co.uk/dev/asteroids
3 // Used with permission, including the sound and bitmap assets.
4
5 // This should really be multiple files but the embedder doesn't support
6 // parts yet. I concatenated the parts in a somewhat random order.
7 //
8 // Note that Skia seems to have issues with the render compositing modes, so
9 // explosions look a bit messy; they aren't transparent where they should be.
10 //
11 // Currently we use the accelerometer on the phone for direction and thrust.
12 // This is hard to control and should probably be changed. The game is also a
13 // bit janky on the phone.
14
15 library asteroids;
vsm 2013/04/01 14:00:17 Consider breaking this file up - perhaps one file
16
17 import 'dart:math' as Math;
18 import 'gl.dart';
19
20 const RAD = Math.PI/180.0;
vsm 2013/04/01 14:00:17 Nit: spaces around arithmetic ops.
21 const PI = Math.PI;
22 const TWOPI = Math.PI*2;
23 const ONEOPI = 1.0 / Math.PI;
24 const PIO2 = Math.PI/2;
25 const PIO4 = Math.PI/4;
26 const PIO8 = Math.PI/8;
27 const PIO16 = Math.PI/16;
28 const PIO32 = Math.PI/32;
29
30 var _rnd = new Math.Random();
31 double random() => _rnd.nextDouble();
32 int randomInt(int min, int max) => min + _rnd.nextInt(max - min + 1);
33
34 class KEY {
vsm 2013/04/01 14:00:17 KEY -> Key per style?
35 static const int SHIFT = 16;
36 static const int CTRL = 17;
37 static const int ESC = 27;
38 static const int RIGHT = 39;
39 static const int UP = 38;
40 static const int LEFT = 37;
41 static const int DOWN = 40;
42 static const int SPACE = 32;
43 static const int A = 65;
44 static const int E = 69;
45 static const int G = 71;
46 static const int L = 76;
47 static const int P = 80;
48 static const int R = 82;
49 static const int S = 83;
50 static const int Z = 90;
51 }
52
53 // Globals
54 var DEBUG = {
vsm 2013/04/01 14:00:17 const DEBUG or var debug
55 'enabled': false,
56 'invincible': false,
57 'collisionRadius': false,
58 'fps': true
59 };
60
61 var GLOWEFFECT = true;
vsm 2013/04/01 14:00:17 const or lowercase for these 3
62 var GLOWSHADOWBLUR = 8;
63 var SCOREDBKEY = "asteroids-score-1.1";
64
65 var g_asteroidImgs = [];
vsm 2013/04/01 14:00:17 'g_' -> '_' ?
66 var g_shieldImg = new ImageElement();
67 var g_backgroundImg = new ImageElement();
68 var g_playerImg = new ImageElement();
69 var g_enemyshipImg = new ImageElement();
70 var soundManager;
71
72 /** Asteroids colour constants */
73 class Colours {
vsm 2013/04/01 14:00:17 We've been using American spelling (Color) in the
74 static const PARTICLE = "rgb(255,125,50)";
75 static const ENEMY_SHIP = "rgb(200,200,250)";
76 static const ENEMY_SHIP_DARK = "rgb(150,150,200)";
77 static const GREEN_LASER = "rgb(120,255,120)";
78 static const GREEN_LASER_DARK = "rgb(50,255,50)";
79 static const GREEN_LASERX2 = "rgb(120,255,150)";
80 static const GREEN_LASERX2_DARK = "rgb(50,255,75)";
81 static const PLAYER_BOMB = "rgb(155,255,155)";
82 static const PLAYER_THRUST = "rgb(25,125,255)";
83 static const PLAYER_SHIELD = "rgb(100,100,255)";
84 }
85
86 /**
87 * Actor base class.
88 *
89 * Game actors have a position in the game world and a current vector to
90 * indicate direction and speed of travel per frame. They each support the
91 * onUpdate() and onRender() event methods, finally an actor has an expired()
92 * method which should return true when the actor object should be removed
93 * from play.
94 */
95 class Actor {
96 Vector position, velocity;
97
98 Actor(this.position, this.velocity);
99
100 /**
101 * Actor game loop update event method. Called for each actor
102 * at the start of each game loop cycle.
103 */
104 onUpdate(Scene scene) {}
105
106 /**
107 * Actor rendering event method. Called for each actor to
108 * render for each frame.
109 */
110 void onRender(CanvasRenderingContext2D ctx) {}
111
112 /**
113 * Actor expiration test; return true if expired and to be removed
114 * from the actor list, false if still in play.
115 */
116 bool expired() => false;
117
118 get frameMultiplier => GameHandler.frameMultiplier;
119 get frameStart => GameHandler.frameStart;
120 get canvas_height => GameHandler.height;
121 get canvas_width => GameHandler.width;
122 }
123
124 // Short-lived actors (like particles and munitions). These have a
125 // start time and lifespan, and fade out after a period.
126
127 class ShortLivedActor extends Actor {
128 int lifespan;
129 int start;
130
131 ShortLivedActor(Vector position, Vector velocity,
132 this.lifespan)
133 : super(position, velocity),
134 this.start = GameHandler.frameStart;
135
136 bool expired() => (frameStart - start > lifespan);
137
138 /**
139 * Helper to return a value multiplied by the ratio of the remaining lifespan
140 */
141 double fadeValue(double val, int offset) {
142 var rem = lifespan - (frameStart - start),
143 result = val;
144 if (rem < offset) {
145 result = (val / offset) * rem;
146 result = Math.max(0.0, Math.min(result, val));
147 }
148 return result;
149 }
150 }
151
152 class AttractorScene extends Scene {
153 AsteroidsMain game;
154
155 AttractorScene(this.game)
156 : super(false, null) {
157 }
158
159 bool start = false;
160 bool imagesLoaded = false;
161 double sine = 0.0;
162 double mult = 0.0;
163 double multIncrement = 0.0;
164 List actors = null;
165 int SCENE_LENGTH = 400;
166 int SCENE_FADE = 75;
vsm 2013/04/01 14:00:17 const or lowercase on these two.
167 List sceneRenderers = null;
168 int currentSceneRenderer = 0;
169 int currentSceneFrame = 0;
170
171 bool isComplete() => start;
172
173 void onInitScene() {
174 start = false;
175 mult = 512.0;
176 multIncrement = 0.5;
177 currentSceneRenderer = 0;
178 currentSceneFrame = 0;
179
180 // scene renderers
181 // display welcome text, info text and high scores
182 sceneRenderers = [
183 sceneRendererWelcome,
184 sceneRendererInfo,
185 sceneRendererScores ];
186
187 // randomly generate some background asteroids for attractor scene
188 actors = [];
189 for (var i = 0; i < 8; i++) {
190 var pos = new Vector(random() * GameHandler.width.toDouble(),
191 random() * GameHandler.height.toDouble());
192 var vec = new Vector(((random() * 2.0) - 1.0), ((random() * 2.0) - 1.0));
193 actors.add(new Asteroid(pos, vec, randomInt(3, 4)));
194 }
195
196 game.score = 0;
197 game.lives = 3;
198 }
199
200 void onRenderScene(CanvasRenderingContext2D ctx) {
201 if (imagesLoaded) {
202 // Draw the background asteroids.
203 for (var i = 0; i < actors.length; i++) {
204 var actor = actors[i];
205 actor.onUpdate(this);
206 game.updateActorPosition(actor);
207 actor.onRender(ctx);
208 }
209
210 // Handle cycling through scenes.
211 if (++currentSceneFrame == SCENE_LENGTH) { // Move to next scene.
212 if (++currentSceneRenderer == sceneRenderers.length) {
213 currentSceneRenderer = 0; // Wrap to first scene.
214 }
215 currentSceneFrame = 0;
216 }
217
218 ctx.save();
219
220 // fade in/out
221 if (currentSceneFrame < SCENE_FADE) {
222 // fading in
223 ctx.globalAlpha = 1 - ((SCENE_FADE - currentSceneFrame) / SCENE_FADE);
224 } else if (currentSceneFrame >= SCENE_LENGTH - SCENE_FADE) {
225 // fading out
226 ctx.globalAlpha = ((SCENE_LENGTH - currentSceneFrame) / SCENE_FADE);
227 } else {
228 ctx.globalAlpha = 1.0;
229 }
230
231 sceneRenderers[currentSceneRenderer](ctx);
232
233 ctx.restore();
234
235 sineText(ctx, "BLASTEROIDS",
236 GameHandler.width ~/ 2 - 130, GameHandler.height ~/ 2 - 64);
237 } else {
238 centerFillText(ctx, "Loading...",
239 "18pt Courier New", GameHandler.height ~/ 2, "white");
240 }
241 }
242
243 void sceneRendererWelcome(CanvasRenderingContext2D ctx) {
244 ctx.fillStyle = ctx.strokeStyle = "white";
245 centerFillText(ctx, "Press SPACE or click to start", "18pt Courier New",
246 GameHandler.height ~/ 2);
247 fillText(ctx, "based on Javascript game by Kevin Roast",
248 "10pt Courier New", 16, 624);
249 }
250
251 void sceneRendererInfo(CanvasRenderingContext2D ctx) {
252 ctx.fillStyle = ctx.strokeStyle = "white";
253 fillText(ctx, "How to play...", "14pt Courier New", 40, 320);
254 fillText(ctx, "Arrow keys or tilt to rotate, thrust, shield. "
255 "SPACE or touch to fire.",
256 "14pt Courier New", 40, 350);
257 fillText(ctx, "Pickup the glowing power-ups to enhance your ship.",
258 "14pt Courier New", 40, 370);
259 fillText(ctx, "Watch out for enemy saucers!", "14pt Courier New", 40, 390);
260 }
261
262 void sceneRendererScores(CanvasRenderingContext2D ctx) {
263 ctx.fillStyle = ctx.strokeStyle = "white";
264 centerFillText(ctx, "High Score", "18pt Courier New", 320);
265 var sscore = this.game.highscore.toString();
266 // pad with zeros
267 for (var i=0, j=8-sscore.length; i<j; i++) {
268 sscore = "0$sscore";
269 }
270 centerFillText(ctx, sscore, "18pt Courier New", 350);
271 }
272
273 /** Callback from image preloader when all images are ready */
274 void ready() {
275 imagesLoaded = true;
276 }
277
278 /**
279 * Render the a text string in a pulsing x-sine y-cos wave pattern
280 * The multiplier for the sinewave is modulated over time
281 */
282 void sineText(CanvasRenderingContext2D ctx, String txt, int xpos, int ypos) {
283 mult += multIncrement;
284 if (mult > 1024.0) {
285 multIncrement = -multIncrement;
286 } else if (this.mult < 128.0) {
287 multIncrement = -multIncrement;
288 }
289 var offset = sine;
290 for (var i = 0; i < txt.length; i++) {
291 var y = ypos + ((Math.sin(offset) * RAD) * mult).toInt();
292 var x = xpos + ((Math.cos(offset++) * RAD) * (mult * 0.5)).toInt();
293 fillText(ctx, txt[i], "36pt Courier New", x + i * 30, y, "white");
294 }
295 sine += 0.075;
296 }
297
298 bool onKeyDownHandler(int keyCode) {
299 log("In onKeyDownHandler, AttractorScene");
300 switch (keyCode) {
301 case KEY.SPACE:
302 if (imagesLoaded) {
303 start = true;
304 }
305 return true;
306 case KEY.ESC:
307 GameHandler.togglePause();
308 return true;
309 }
310 return false;
311 }
312
313 bool onMouseDownHandler(e) {
314 if (imagesLoaded) {
315 start = true;
316 }
317 return true;
318 }
319 }
320
321 /**
322 * An actor representing a transient effect in the game world. An effect is
323 * nothing more than a special graphic that does not play any direct part in
324 * the game and does not interact with any other objects. It automatically
325 * expires after a set lifespan, generally the rendering of the effect is
326 * based on the remaining lifespan.
327 */
328 class EffectActor extends Actor {
329 int lifespan; // in msec.
330 int effectStart; // start time
331
332 EffectActor(Vector position , Vector velocity, [this.lifespan = 0])
333 : super(position, velocity) {
334 effectStart = frameStart;
335 }
336
337 bool expired() => (frameStart - effectStart > lifespan);
338
339 /**
340 * Helper for an effect to return the value multiplied by the ratio of the
341 * remaining lifespan of the effect.
342 */
343 double effectValue(double val) {
344 var result = val - (val * (frameStart - effectStart)) / lifespan;
345 return Math.max(0.0, Math.min(val, result));
346 }
347 }
348
349 /** Text indicator effect actor class. */
350 class TextIndicator extends EffectActor {
351 int fadeLength;
352 int textSize;
353 String msg;
354 String colour;
vsm 2013/04/01 14:00:17 colour -> color
355
356 TextIndicator(Vector position, Vector velocity, this.msg,
357 [this.textSize = 12, this.colour = "white",
358 int fl = 500]) :
359 super(position, velocity, fl), fadeLength = fl;
360
361 const int DEFAULT_FADE_LENGTH = 500;
362
363
364 void onRender(CanvasRenderingContext2D ctx) {
365 // Fade out alpha.
366 ctx.save();
367 ctx.globalAlpha = effectValue(1.0);
368 fillText(ctx, msg, "${textSize}pt Courier New",
369 position.x, position.y, colour);
370 ctx.restore();
371 }
372 }
373
374 /** Score indicator effect actor class. */
375 class ScoreIndicator extends TextIndicator {
376 ScoreIndicator(Vector position, Vector velocity, int score,
377 [int textSize = 12, String prefix = '', String colour = "white",
378 int fadeLength = 500]) :
379 super(position, velocity, '${prefix.length > 0 ? "$prefix " : ""}${score}',
380 textSize, colour, fadeLength);
381 }
382
383 /** Power up collectable. */
384 class PowerUp extends EffectActor {
385 PowerUp(Vector position, Vector velocity)
386 : super(position, velocity);
387
388 const int RADIUS = 8;
389 int pulse = 128;
390 int pulseinc = 5;
391
392 void onRender(CanvasRenderingContext2D ctx) {
393 ctx.save();
394 ctx.globalAlpha = 0.75;
395 var col = "rgb(255,${pulse.toString()},0)";
396 ctx.fillStyle = col;
397 ctx.strokeStyle = "rgb(255,255,128)";
398 ctx.beginPath();
399 ctx.arc(position.x, position.y, RADIUS, 0, TWOPI, true);
400 ctx.closePath();
401 ctx.fill();
402 ctx.stroke();
403 ctx.restore();
404 pulse += pulseinc;
405 if (pulse > 255){
406 pulse = 256 - pulseinc;
407 pulseinc =- pulseinc;
408 } else if (pulse < 0) {
409 pulse = 0 - pulseinc;
410 pulseinc =- pulseinc;
411 }
412 }
413
414 get radius => RADIUS;
415
416 void collected(AsteroidsMain game, Player player, GameScene scene) {
417 // Rrandomly select a powerup to apply.
vsm 2013/04/01 14:00:17 Rrandomly -> Randomly
418 var message = null;
419 var n, m, enemy, pos;
420 switch (randomInt(0, 9)) {
421 case 0:
422 case 1:
423 message = "Energy Boost!";
424 player.energy += player.ENERGY_INIT / 2;
425 if (player.energy > player.ENERGY_INIT) {
426 player.energy = player.ENERGY_INIT;
427 }
428 break;
429
430 case 2:
431 message = "Fire When Shielded!";
432 player.fireWhenShield = true;
433 break;
434
435 case 3:
436 message = "Extra Life!";
437 game.lives++;
438 break;
439
440 case 4:
441 message = "Slow Down Asteroids!";
442 m = scene.enemies.length;
443 for (n = 0; n < m; n++) {
444 enemy = scene.enemies[n];
445 if (enemy is Asteroid) {
446 enemy.velocity.scale(0.66);
447 }
448 }
449 break;
450
451 case 5:
452 message = "Smart Bomb!";
453
454 var effectRad = 96;
455
456 // Aadd a BIG explosion actor at the smart bomb weapon position and vect or
vsm 2013/04/01 14:00:17 line len
457 var boom = new Explosion(position.clone(),
458 velocity.nscale(0.5), effectRad / 8);
459 scene.effects.add(boom);
460
461 // Test circle intersection with each enemy actor.
462 // We check the enemy list length each iteration to catch baby asteroids
463 // this is a fully fledged smart bomb after all!
464 pos = position;
465 for (n = 0; n < scene.enemies.length; n++) {
466 enemy = scene.enemies[n];
467
468 // Test the distance against the two radius combined.
469 if (pos.distance(enemy.position) <= effectRad + enemy.radius) {
470 // Intersection detected!
471 enemy.hit(-1);
472 scene.generatePowerUp(enemy);
473 scene.destroyEnemy(enemy, velocity, true);
474 }
475 }
476 break;
477
478 case 6:
479 message = "Twin Cannons!";
480 player.primaryWeapons["main"] = new TwinCannonsWeapon(player);
481 break;
482
483 case 7:
484 message = "Spray Cannons!";
485 player.primaryWeapons["main"] = new VSprayCannonsWeapon(player);
486 break;
487
488 case 8:
489 message = "Rear Gun!";
490 player.primaryWeapons["rear"] = new RearGunWeapon(player);
491 break;
492
493 case 9:
494 message = "Side Guns!";
495 player.primaryWeapons["side"] = new SideGunWeapon(player);
496 break;
497 }
498
499 if (message != null) {
500 // Generate a effect indicator at the destroyed enemy position.
501 var vec = new Vector(0.0, -1.5);
502 var effect = new TextIndicator(
503 new Vector(position.x, position.y - RADIUS), vec,
504 message, null, null, 700);
505 scene.effects.add(effect);
506 }
507 }
508 }
509 /**
510 * This is the common base class of actors that can be hit and destroyed by
511 * player bullets. It supports a hit() method which should return true when
512 * the enemy object should be removed from play.
513 */
514 class EnemyActor extends SpriteActor {
515 EnemyActor(Vector position, Vector velocity, this.size)
516 : super(position, velocity);
517
518 bool alive = true;
519
520 /** Size - values from 1-4 are valid for asteroids, 0-1 for ships. */
521 int size;
522
523 bool expired() => !alive;
524
525 bool hit(num force) {
526 alive = false;
527 return true;
528 }
529 }
530
531 /**
532 * Asteroid actor class.
533 */
534 class Asteroid extends EnemyActor {
535 Asteroid(Vector position, Vector velocity, int size, [this.type])
536 : super(position, velocity, size) {
537 health = size;
538
539 // Randomly select an asteroid image bitmap.
540 if (type == null) {
541 type = randomInt(1, 4);
542 }
543 animImage = g_asteroidImgs[type-1];
544
545 // Rrandomly setup animation speed and direction.
546 animForward = (random() < 0.5);
547 animSpeed = 0.3 + random() * 0.5;
548 animLength = ANIMATION_LENGTH;
549 rotation = randomInt(0, 180);
550 rotationSpeed = (random() - 0.5) / 30;
551 }
552
553 const int ANIMATION_LENGTH = 180;
554
555 /** Asteroid graphic type i.e. which bitmap it is drawn from. */
556 int type;
557
558 /** Asteroid health before it's destroyed. */
559 num health = 0;
560
561 /** Retro graphics mode rotation orientation and speed. */
562 int rotation = 0;
563 double rotationSpeed = 0.0;
564
565 /** Asteroid rendering method. */
566 void onRender(CanvasRenderingContext2D ctx) {
567 var rad = size * 8;
568 ctx.save();
569 // Render asteroid graphic bitmap. The bitmap is rendered slightly large
570 // than the radius as the raytraced asteroid graphics do not quite touch
571 // the edges of the 64x64 sprite - this improves perceived collision
572 // detection.
573 //print("Position ${position}, Vector ${vector}");
574 //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.
575 renderSprite(ctx, position.x - rad - 2, position.y - rad - 2, (rad * 2)+4);
576 ctx.restore();
577 }
578
579 get radius => size * 8;
580
581 bool hit(num force) {
582 if (force != -1) {
583 health -= force;
584 } else {
585 // instant kill
586 health = 0;
587 }
588 return !(alive = (health > 0));
589 }
590 }
591
592 /** Enemy Ship actor class. */
593 class EnemyShip extends EnemyActor {
594
595 get radius => _radius;
596
597 EnemyShip(GameScene scene, int size)
598 : super(null, null, size) {
599 // Small ship, alter settings slightly.
600 if (size == 1) {
601 BULLET_RECHARGE_MS = 1300;
602 _radius = 8;
603 } else {
604 _radius = 16;
605 }
606
607 // Randomly setup enemy initial position and vector
608 // ensure the enemy starts in the opposite quadrant to the player.
609 var p, v;
610 if (scene.player.position.x < canvas_width / 2) {
611 // Player on left of the screen.
612 if (scene.player.position.y < canvas_height / 2) {
613 // Player in top left of the screen.
614 position = new Vector(canvas_width-48, canvas_height-48);
615 } else {
616 // Player in bottom left of the screen.
617 position = new Vector(canvas_width-48, 48);
618 }
619 velocity = new Vector(-(random() + 0.25 + size * 0.75),
620 random() + 0.25 + size * 0.75);
621 } else {
622 // Player on right of the screen.
623 if (scene.player.position.y < canvas_height / 2) {
624 // Player in top right of the screen.
625 position = new Vector(0, canvas_height-48);
626 } else {
627 // Player in bottom right of the screen.
628 position = new Vector(0, 48);
629 }
630 velocity = new Vector(random() + 0.25 + size * 0.75,
631 random() + 0.25 + size * 0.75);
632 }
633
634 // Setup SpriteActor values.
635 animImage = g_enemyshipImg;
636 animLength = SHIP_ANIM_LENGTH;
637 }
638
639 const int SHIP_ANIM_LENGTH = 90;
640 int _radius;
641 int BULLET_RECHARGE_MS = 1800;
642
643
644 /** True if ship alive, false if ready for expiration. */
645 bool alive = true;
646
647 /** Bullet fire recharging counter. */
648 int bulletRecharge = 0;
649
650 void onUpdate(GameScene scene) {
651 // change enemy direction randomly
652 if (size == 0) {
653 if (random() < 0.01) {
654 velocity.y = -(velocity.y + (0.25 - (random()/2)));
655 }
656 } else {
657 if (random() < 0.02) {
658 velocity.y = -(velocity.y + (0.5 - random()));
659 }
660 }
661
662 // regular fire a bullet at the player
663 if (frameStart - bulletRecharge >
664 BULLET_RECHARGE_MS && scene.player.alive) {
665 // ok, update last fired time and we can now generate a bullet
666 bulletRecharge = frameStart;
667
668 // generate a vector pointed at the player
669 // by calculating a vector between the player and enemy positions
670 var v = scene.player.position.clone().sub(position);
671 // scale resulting vector down to bullet vector size
672 var scale = (size == 0 ? 3.0 : 3.5) / v.length();
673 v.x *= scale;
674 v.y *= scale;
675 // slightly randomize the direction (big ship is less accurate also)
676 v.x += (size == 0 ? (random() * 2.0 - 1.0) : (random() - 0.5));
677 v.y += (size == 0 ? (random() * 2.0 - 1.0) : (random() - 0.5));
678 // - could add the enemy motion vector for correct momentum
679 // - but this leads to slow bullets firing back from dir of travel
680 // - so pretend that enemies are clever enough to account for this...
681 //v.add(this.vector);
682
683 var bullet = new EnemyBullet(position.clone(), v);
684 scene.enemyBullets.add(bullet);
685 //soundManager.play('enemy_bomb');
686 }
687 }
688
689 /** Enemy rendering method. */
690 void onRender(CanvasRenderingContext2D ctx) {
691 // render enemy graphic bitmap
692 var rad = radius + 2;
693 renderSprite(ctx, position.x - rad, position.y - rad, rad * 2);
694 }
695
696 /** Enemy hit by a bullet; return true if destroyed, false otherwise. */
697 bool hit(num force) {
698 alive = false;
699 return true;
700 }
701
702 bool expired() {
703 return !alive;
704 }
705 }
706
707 class GameCompleted extends Scene {
708 AsteroidsMain game;
709 var player;
710
711 GameCompleted(this.game)
712 : super(false) {
713 interval = new Interval("CONGRATULATIONS!", intervalRenderer);
714 player = game.player;
715 }
716
717 bool isComplete() => true;
718
719 void intervalRenderer(Interval interval, CanvasRenderingContext2D ctx) {
720 if (interval.framecounter++ == 0) {
721 if (game.score == game.highscore) {
722 // save new high score to HTML5 local storage
723 /* if (localStorage) {
vsm 2013/04/01 14:00:17 Delete commented code or add TODO.
724 localStorage.setItem(SCOREDBKEY, this.game.score);
725 }
726 */
727 }
728 }
729 if (interval.framecounter < 1000) {
730 fillText(ctx, interval.label, "18pt Courier New",
731 GameHandler.width ~/ 2 - 96, GameHandler.height ~/ 2 - 32, "white");
732 fillText(ctx, "Score: ${game.score}", "14pt Courier New",
733 GameHandler.width ~/ 2 - 64, GameHandler.height ~/ 2, "white");
734 if (game.score == game.highscore) {
735 fillText(ctx, "New High Score!", "14pt Courier New",
736 GameHandler.width ~/ 2 - 64, GameHandler.height ~/ 2 + 24, "white");
737 }
738 } else {
739 interval.complete = true;
740 }
741 }
742 }
743
744 /**
745 * Game Handler.
746 *
747 * Singleton instance responsible for managing the main game loop and
748 * maintaining a few global references such as the canvas and frame counters.
749 */
750 class GameHandler {
751 /**
752 * The single Game.Main derived instance
753 */
754 static GameMain game = null;
755
756 static bool paused = false;
757 static CanvasElement canvas = null;
758 static int width = 0;
759 static int height = 0;
760 static int frameCount = 0;
761
762 /** Frame multiplier - i.e. against the ideal fps. */
763 static double frameMultiplier = 1.0;
764
765 /** Last frame start time in ms. */
766 static int frameStart = 0;
767
768 /** Debugging output. */
769 static int maxfps = 0;
770
771 /** Ideal FPS constant. */
772 static const double FPSMS = 1000/60;
vsm 2013/04/01 14:00:17 You can omit the "double". Spacing around '/'.
773
774 static Prerenderer bitmaps;
775
776 /** Init function called once by your window.onload handler. */
777 static void init(c) {
778 canvas = c;
779 width = canvas.width;
780 height = canvas.height;
781 log("INit GameMain($c,$width,$height)");
vsm 2013/04/01 14:00:17 INit -> Init
782 }
783
784 /**
785 * Game start method - begins the main game loop.
786 * Pass in the object that represent the game to execute.
787 */
788 static void start(GameMain g) {
789 game = g;
790 frameStart = new DateTime.now().millisecondsSinceEpoch;
791 log("Doing first frame");
792 game.frame();
793 }
794
795 /** Called each frame by the main game loop unless paused. */
796 static void doFrame(_) {
797 log("Doing next frame");
798 game.frame();
799 }
800
801 static void togglePause() {
802 if (paused) {
803 paused = false;
804 frameStart = new DateTime.now().millisecondsSinceEpoch;
805 game.frame();
806 } else {
807 paused = true;
808 }
809 }
810
811 static bool onAccelerometer(double x, double y, double z) {
812 return game == null ? true : game.onAccelerometer(x, y, z);
813 }
814 }
815
816 bool onAccelerometer(double x, double y, double z) {
817 return GameHandler.onAccelerometer(x, y, z);
818 }
819
820 /** Game main loop class. */
821 class GameMain {
822
823 GameMain() {
824 var me = this;
825
826 document.onKeyDown.listen((KeyboardEvent event) {
827 var keyCode = event.keyCode;
828
829 log("In document.onKeyDown($keyCode)");
830 if (me.sceneIndex != -1) {
831 if (me.scenes[me.sceneIndex].onKeyDownHandler(keyCode) != null) {
832 // if the key is handled, prevent any further events
833 if (event != null) {
834 event.preventDefault();
835 event.stopPropagation();
836 }
837 }
838 }
839 });
840
841 document.onKeyUp.listen((KeyboardEvent event) {
842 var keyCode = event.keyCode;
843 if (me.sceneIndex != -1) {
844 if (me.scenes[me.sceneIndex].onKeyUpHandler(keyCode) != null) {
845 // if the key is handled, prevent any further events
846 if (event != null) {
847 event.preventDefault();
848 event.stopPropagation();
849 }
850 }
851 }
852 });
853
854 document.onMouseDown.listen((MouseEvent event) {
855 if (me.sceneIndex != -1) {
856 if (me.scenes[me.sceneIndex].onMouseDownHandler(event) != null) {
857 // if the event is handled, prevent any further events
858 if (event != null) {
859 event.preventDefault();
860 event.stopPropagation();
861 }
862 }
863 }
864 });
865
866 document.onMouseUp.listen((MouseEvent event) {
867 if (me.sceneIndex != -1) {
868 if (me.scenes[me.sceneIndex].onMouseUpHandler(event) != null) {
869 // if the event is handled, prevent any further events
870 if (event != null) {
871 event.preventDefault();
872 event.stopPropagation();
873 }
874 }
875 }
876 });
877
878 }
879
880 List scenes = [];
881 Scene startScene = null;
882 Scene endScene = null;
883 Scene currentScene = null;
884 int sceneIndex = -1;
885 var interval = null;
886 int totalFrames = 0;
887
888 bool onAccelerometer(double x, double y, double z) {
889 if (currentScene != null) {
890 return currentScene.onAccelerometer(x, y, z);
891 }
892 return true;
893 }
894 /**
895 * Game frame execute method - called by anim handler timeout
896 */
897 void frame() {
898 var frameStart = new DateTime.now().millisecondsSinceEpoch;
899
900 // Calculate scene transition and current scene.
901 if (currentScene == null) {
902 // Set to scene zero (game init).
903 currentScene = scenes[sceneIndex = 0];
904 currentScene.onInitScene();
905 } else if (isGameOver()) {
906 sceneIndex = -1;
907 currentScene = endScene;
908 currentScene.onInitScene();
909 }
910
911 if ((currentScene.interval == null ||
912 currentScene.interval.complete) && currentScene.isComplete()) {
913 if (++sceneIndex >= scenes.length){
914 sceneIndex = 0;
915 }
916 currentScene = scenes[sceneIndex];
917 currentScene.onInitScene();
918 }
919
920 var ctx = GameHandler.canvas.getContext('2d');
921
922 // Rrender the game and current scene.
923 ctx.save();
924 if (currentScene.interval == null || currentScene.interval.complete) {
925 currentScene.onBeforeRenderScene();
926 onRenderGame(ctx);
927 currentScene.onRenderScene(ctx);
928 } else {
929 onRenderGame(ctx);
930 currentScene.interval.intervalRenderer(currentScene.interval, ctx);
931 }
932 ctx.restore();
933
934 GameHandler.frameCount++;
935
936 // Calculate frame total time interval and frame multiplier required
937 // for smooth animation.
938
939 // Time since last frame.
940 var frameInterval = frameStart - GameHandler.frameStart;
941 if (frameInterval == 0) frameInterval = 1;
942 if (GameHandler.frameCount % 16 == 0) { // Update fps every 16 frames
943 GameHandler.maxfps = (1000 / frameInterval).floor().toInt();
944 }
945 GameHandler.frameMultiplier = frameInterval.toDouble() / GameHandler.FPSMS;
946
947 GameHandler.frameStart = frameStart;
948
949 if (!GameHandler.paused) {
950 window.requestAnimationFrame(GameHandler.doFrame);
951 }
952 if ((++totalFrames % 600) == 0) {
953 log('${totalFrames} frames; multiplier ${GameHandler.frameMultiplier}');
954 }
955 }
956
957 void onRenderGame(CanvasRenderingContext2D ctx) {}
958
959 bool isGameOver() => false;
960 }
961
962 class AsteroidsMain extends GameMain {
963
964 AsteroidsMain() : super() {
965 var attractorScene = new AttractorScene(this);
966
967 // get the images graphics loading
968 var loader = new Preloader();
969 loader.addImage(g_playerImg, 'player.png');
970 loader.addImage(g_asteroidImgs[0], 'asteroid1.png');
971 loader.addImage(g_asteroidImgs[1], 'asteroid2.png');
972 loader.addImage(g_asteroidImgs[2], 'asteroid3.png');
973 loader.addImage(g_asteroidImgs[3], 'asteroid4.png');
974 loader.addImage(g_shieldImg, 'shield.png');
975 loader.addImage(g_enemyshipImg, 'enemyship1.png');
976
977 // The attactor scene is displayed first and responsible for allowing the
978 // player to start the game once all images have been loaded.
979 loader.onLoadCallback(() {
980 attractorScene.ready();
981 });
982
983 // Generate the single player actor - available across all scenes.
984 player = new Player(
985 new Vector(GameHandler.width / 2, GameHandler.height / 2),
986 new Vector(0.0, 0.0),
987 0.0);
988
989 scenes.add(attractorScene);
990
991 for (var i = 0; i < 12; i++){
992 var level = new GameScene(this, i+1);
993 scenes.add(level);
994 }
995
996 scenes.add(new GameCompleted(this));
997
998 // Set special end scene member value to a Game Over scene.
999 endScene = new GameOverScene(this);
1000
1001 if (window.localStorage.containsKey(SCOREDBKEY)) {
1002 highscore = int.parse(window.localStorage[SCOREDBKEY]);
1003 }
1004 // Perform prerender steps - create some bitmap graphics to use later.
1005 GameHandler.bitmaps = new Prerenderer();
1006 GameHandler.bitmaps.execute();
1007 }
1008
1009 Player player = null;
1010 int lives = 0;
1011 int score = 0;
1012 int highscore = 0;
1013 /** Background scrolling bitmap x position */
1014 double backgroundX = 0.0;
1015 /** Background starfield star list */
1016 List starfield = [];
1017
1018 void onRenderGame(CanvasRenderingContext2D ctx) {
1019 // Setup canvas for a render pass and apply background
1020 // draw a scrolling background image.
1021 var w = GameHandler.width;
1022 var h = GameHandler.height;
1023 //var sourceRect = new Rect(backgroundX, 0, w, h);
1024 //var destRect = new Rect(0, 0, w, h);
1025 //ctx.drawImageToRect(g_backgroundImg, destRect,
1026 // sourceRect:sourceRect);
1027 ctx.drawImageScaledFromSource(g_backgroundImg,
1028 backgroundX, 0, w, h, 0, 0, w, h);
1029
1030 backgroundX += (GameHandler.frameMultiplier / 4.0);
1031 if (backgroundX >= g_backgroundImg.width / 2) {
1032 backgroundX -= g_backgroundImg.width / 2;
1033 }
1034 ctx.shadowBlur = 0;
1035 }
1036
1037 bool isGameOver() {
1038 if (currentScene is GameScene) {
1039 var gs = currentScene as GameScene;
1040 return (lives == 0 && gs.effects != null && gs.effects.length == 0);
1041 }
1042 return false;
1043 }
1044
1045 /**
1046 * Update an actor position using its current velocity vector.
1047 * Scale the vector by the frame multiplier - this is used to ensure
1048 * all actors move the same distance over time regardles of framerate.
1049 * Also handle traversing out of the coordinate space and back again.
1050 */
1051 void updateActorPosition(Actor actor) {
1052 actor.position.add(actor.velocity.nscale(GameHandler.frameMultiplier));
1053 actor.position.wrap(0, GameHandler.width - 1, 0, GameHandler.height - 1);
1054 }
1055 }
1056
1057 class GameOverScene extends Scene {
1058 var game, player;
1059
1060 GameOverScene(this.game) :
1061 super(false) {
1062 interval = new Interval("GAME OVER", intervalRenderer);
1063 player = game.player;
1064 }
1065
1066 bool isComplete() => true;
1067
1068 void intervalRenderer(Interval interval, CanvasRenderingContext2D ctx) {
1069 if (interval.framecounter++ == 0) {
1070 if (game.score == game.highscore) {
1071 window.localStorage[SCOREDBKEY] = game.score.toString();
1072 }
1073 }
1074 if (interval.framecounter < 300) {
1075 fillText(ctx, interval.label, "18pt Courier New",
1076 GameHandler.width * 0.5 - 64, GameHandler.height*0.5 - 32, "white");
1077 fillText(ctx, "Score: ${game.score}", "14pt Courier New",
1078 GameHandler.width * 0.5 - 64, GameHandler.height*0.5, "white");
1079 if (game.score == game.highscore) {
1080 fillText(ctx, "New High Score!", "14pt Courier New",
1081 GameHandler.width * 0.5 - 64, GameHandler.height*0.5 + 24, "white");
1082 }
1083 } else {
1084 interval.complete = true;
1085 }
1086 }
1087 }
1088
1089 class GameScene extends Scene {
1090 AsteroidsMain game;
1091 int wave;
1092 var player;
1093 List actors = null;
1094 List playerBullets = null;
1095 List enemies = null;
1096 List enemyBullets = null;
1097 List effects = null;
1098 List collectables = null;
1099 int enemyShipCount = 0;
1100 int enemyShipAdded = 0;
1101 int scoredisplay = 0;
1102 bool skipLevel = false;
1103
1104 Input input;
1105
1106 GameScene(this.game, this.wave)
1107 : super(true) {
1108 interval = new Interval("Wave ${wave}", intervalRenderer);
1109 player = game.player;
1110 input = new Input();
1111 }
1112
1113 void onInitScene() {
1114 // Generate the actors and add the actor sub-lists to the main actor list.
1115 actors = [];
1116 enemies = [];
1117 actors.add(enemies);
1118 actors.add(playerBullets = []);
1119 actors.add(enemyBullets = []);
1120 actors.add(effects = []);
1121 actors.add(collectables = []);
1122
1123 // Reset player ready for game restart.
1124 resetPlayerActor(wave != 1);
1125
1126 // Randomly generate some asteroids.
1127 var factor = 1.0 + ((wave - 1) * 0.075);
1128 for (var i=1, j=(4 + wave); i < j; i++) {
1129 enemies.add(generateAsteroid(factor));
1130 }
1131
1132 // Reset enemy ship count and last enemy added time.
1133 enemyShipAdded = GameHandler.frameStart;
1134 enemyShipCount = 0;
1135
1136 // Reset interval flag.
1137 interval.reset();
1138 skipLevel = false;
1139 }
1140
1141 /** Restore the player to the game - reseting position etc. */
1142 void resetPlayerActor(bool persistPowerUps) {
1143 actors.add([player]);
1144
1145 // Reset the player position.
1146 player.position.x = GameHandler.width / 2;
1147 player.position.y = GameHandler.height / 2;
1148 player.velocity.x = 0.0;
1149 player.velocity.y = 0.0;
1150 player.heading = 0.0;
1151 player.reset(persistPowerUps);
1152
1153 // Reset keyboard input values.
1154 input.reset();
1155 }
1156
1157 /** Scene before rendering event handler. */
1158 void onBeforeRenderScene() {
1159 // Handle key input.
1160 if (input.left) {
1161 // Rotate anti-clockwise.
1162 player.heading -= 4 * GameHandler.frameMultiplier;
1163 }
1164 if (input.right) {
1165 // Rotate clockwise.
1166 player.heading += 4 * GameHandler.frameMultiplier;
1167 }
1168 if (input.thrust) {
1169 player.thrust();
1170 }
1171 if (input.shield) {
1172 if (!player.expired()) {
1173 player.activateShield();
1174 }
1175 }
1176 if (input.fireA) {
1177 player.firePrimary(playerBullets);
1178 }
1179 if (input.fireB) {
1180 player.fireSecondary(playerBullets);
1181 }
1182
1183 // Add an enemy every N frames (depending on wave factor).
1184 // Later waves can have 2 ships on screen - earlier waves have one.
1185 if (enemyShipCount <= (wave < 5 ? 0 : 1) &&
1186 GameHandler.frameStart - enemyShipAdded > (20000 - (wave * 1024))) {
1187 enemies.add(new EnemyShip(this, (wave < 3 ? 0 : randomInt(0, 1))));
1188 enemyShipCount++;
1189 enemyShipAdded = GameHandler.frameStart;
1190 }
1191
1192 // Update all actors using their current vector.
1193 updateActors();
1194 }
1195
1196 /** Scene rendering event handler */
1197 void onRenderScene(CanvasRenderingContext2D ctx) {
1198 renderActors(ctx);
1199
1200 if (DEBUG['collisionRadius']) {
1201 renderCollisionRadius(ctx);
1202 }
1203
1204 // Render info overlay graphics.
1205 renderOverlay(ctx);
1206
1207 // Detect bullet collisions.
1208 collisionDetectBullets();
1209
1210 // Detect player collision with asteroids etc.
1211 if (!player.expired()) {
1212 collisionDetectPlayer();
1213 } else {
1214 // If the player died, then respawn after a short delay and
1215 // ensure that they do not instantly collide with an enemy.
1216 if (GameHandler.frameStart - player.killedOn > 3000) {
1217 // Perform a test to check no ememy is close to the player.
1218 var tooClose = false;
1219 var playerPos =
1220 new Vector(GameHandler.width * 0.5, GameHandler.height * 0.5);
1221 for (var i=0, j=this.enemies.length; i<j; i++) {
1222 var enemy = this.enemies[i];
1223 if (playerPos.distance(enemy.position) < 80) {
1224 tooClose = true;
1225 break;
1226 }
1227 }
1228 if (tooClose == false) {
1229 resetPlayerActor(false);
1230 }
1231 }
1232 }
1233 }
1234
1235 bool isComplete() =>
1236 (skipLevel || (enemies.length == 0 && effects.length == 0));
1237
1238 void intervalRenderer(Interval interval, CanvasRenderingContext2D ctx) {
1239 if (interval.framecounter++ < 100) {
1240 fillText(ctx, interval.label, "18pt Courier New",
1241 GameHandler.width*0.5 - 48, GameHandler.height*0.5 - 8, "white");
1242 } else {
1243 interval.complete = true;
1244 }
1245 }
1246
1247 bool onAccelerometer(double x, double y, double z) {
1248 if (input != null) {
1249 input.shield =(x > 2.0);
1250 input.thrust = (x < -1.0);
1251 input.left = (y < -1.5);
1252 input.right = (y > 1.5);
1253 }
1254 return true;
1255 }
1256
1257 bool onMouseDownHandler(e) {
1258 input.fireA = input.fireB = false;
1259 if (e.clientX < GameHandler.width / 3) input.fireB = true;
1260 else if (e.clientX > 2 * GameHandler.width / 3) input.fireA = true;
1261 return true;
1262 }
1263
1264 bool onMouseUpHandler(e) {
1265 input.fireA = input.fireB = false;
1266 return true;
1267 }
1268
1269 bool onKeyDownHandler(int keyCode) {
1270 log("In onKeyDownHandler, GameScene");
1271 switch (keyCode) {
1272 // Note: GLUT doesn't send key up events,
1273 // so the emulator sends key events as down/up pairs,
1274 // which is not what we want. So we have some special
1275 // numeric key handlers here that are distinct for
1276 // up and down to support use with GLUT.
1277 case 52: // '4':
1278 case KEY.LEFT:
1279 input.left = true;
1280 return true;
1281 case 54: // '6'
1282 case KEY.RIGHT:
1283 input.right = true;
1284 return true;
1285 case 56: // '8'
1286 case KEY.UP:
1287 input.thrust = true;
1288 return true;
1289 case 50: // '2'
1290 case KEY.DOWN:
1291 case KEY.SHIFT:
1292 input.shield = true;
1293 return true;
1294 case 48: // '0'
1295 case KEY.SPACE:
1296 input.fireA = true;
1297 return true;
1298 case KEY.Z:
1299 input.fireB = true;
1300 return true;
1301
1302 case KEY.A:
1303 if (DEBUG['enabled']) {
1304 // generate an asteroid
1305 enemies.add(generateAsteroid(1));
1306 return true;
1307 }
1308 break;
1309
1310 case KEY.G:
1311 if (DEBUG['enabled']) {
1312 GLOWEFFECT = !GLOWEFFECT;
1313 return true;
1314 }
1315 break;
1316
1317 case KEY.L:
1318 if (DEBUG['enabled']) {
1319 skipLevel = true;
1320 return true;
1321 }
1322 break;
1323
1324 case KEY.E:
1325 if (DEBUG['enabled']) {
1326 enemies.add(new EnemyShip(this, randomInt(0, 1)));
1327 return true;
1328 }
1329 break;
1330
1331 case KEY.ESC:
1332 GameHandler.togglePause();
1333 return true;
1334 }
1335 return false;
1336 }
1337
1338 bool onKeyUpHandler(int keyCode) {
1339 switch (keyCode) {
1340 case 53: // '5'
1341 input.left = false;
1342 input.right = false;
1343 input.thrust = false;
1344 input.shield = false;
1345 input.fireA = false;
1346 input.fireB = false;
1347 return true;
1348
1349 case KEY.LEFT:
1350 input.left = false;
1351 return true;
1352 case KEY.RIGHT:
1353 input.right = false;
1354 return true;
1355 case KEY.UP:
1356 input.thrust = false;
1357 return true;
1358 case KEY.DOWN:
1359 case KEY.SHIFT:
1360 input.shield = false;
1361 return true;
1362 case KEY.SPACE:
1363 input.fireA = false;
1364 return true;
1365 case KEY.Z:
1366 input.fireB = false;
1367 return true;
1368 }
1369 return false;
1370 }
1371
1372 /**
1373 * Randomly generate a new large asteroid. Ensures the asteroid is not
1374 * generated too close to the player position!
1375 */
1376 Asteroid generateAsteroid(num speedFactor) {
1377 while (true){
1378 // perform a test to check it is not too close to the player
1379 var apos = new Vector(random()*GameHandler.width,
1380 random()*GameHandler.height);
1381 if (player.position.distance(apos) > 125) {
1382 var vec = new Vector( ((random()*2)-1)*speedFactor,
1383 ((random()*2)-1)*speedFactor );
1384 return new Asteroid(apos, vec, 4);
1385 }
1386 }
1387 }
1388
1389 /** Update the actors position based on current vectors and expiration. */
1390 void updateActors() {
1391 for (var i = 0, j = this.actors.length; i < j; i++) {
1392 var actorList = this.actors[i];
1393
1394 for (var n = 0; n < actorList.length; n++) {
1395 var actor = actorList[n];
1396
1397 // call onUpdate() event for each actor
1398 actor.onUpdate(this);
1399
1400 // expiration test first
1401 if (actor.expired()) {
1402 actorList.removeAt(n);
1403 } else {
1404 game.updateActorPosition(actor);
1405 }
1406 }
1407 }
1408 }
1409
1410 /**
1411 * Perform the operation needed to destory the player.
1412 * Mark as killed as reduce lives, explosion effect and play sound.
1413 */
1414 void destroyPlayer() {
1415 // Player destroyed by enemy bullet - remove from play.
1416 player.kill();
1417 game.lives--;
1418 var boom =
1419 new PlayerExplosion(player.position.clone(), player.velocity.clone());
1420 effects.add(boom);
1421 soundManager.play('big_boom');
1422 }
1423
1424 /**
1425 * Detect player collisions with various actor classes
1426 * including Asteroids, Enemies, bullets and collectables
1427 */
1428 void collisionDetectPlayer() {
1429 var playerRadius = player.radius;
1430 var playerPos = player.position;
1431
1432 // Test circle intersection with each asteroid/enemy ship.
1433 for (var n = 0, m = enemies.length; n < m; n++) {
1434 var enemy = enemies[n];
1435
1436 // Calculate distance between the two circles.
1437 if (playerPos.distance(enemy.position) <= playerRadius + enemy.radius) {
1438 // Collision detected.
1439 if (player.isShieldActive()) {
1440 // Remove thrust from the player vector due to collision.
1441 player.velocity.scale(0.75);
1442
1443 // Destroy the enemy - the player is invincible with shield up!
1444 enemy.hit(-1);
1445 destroyEnemy(enemy, player.velocity, true);
1446 } else if (!DEBUG['invincible']) {
1447 destroyPlayer();
1448 }
1449 }
1450 }
1451
1452 // Test intersection with each enemy bullet.
1453 for (var i = 0; i < enemyBullets.length; i++) {
1454 var bullet = enemyBullets[i];
1455
1456 // Calculate distance between the two circles.
1457 if (playerPos.distance(bullet.position) <= playerRadius + bullet.radius) {
1458 // Collision detected.
1459 if (player.isShieldActive()) {
1460 // Remove this bullet from the actor list as it has been destroyed.
1461 enemyBullets.removeAt(i);
1462 } else if (!DEBUG['invincible']) {
1463 destroyPlayer();
1464 }
1465 }
1466 }
1467
1468 // Test intersection with each collectable.
1469 for (var i = 0; i < collectables.length; i++) {
1470 var item = collectables[i];
1471
1472 // Calculate distance between the two circles.
1473 if (playerPos.distance(item.position) <= playerRadius + item.radius) {
1474 // Collision detected - remove item from play and activate it.
1475 collectables.removeAt(i);
1476 item.collected(game, player, this);
1477
1478 soundManager.play('powerup');
1479 }
1480 }
1481 }
1482
1483 /** Detect bullet collisions with asteroids and enemy actors. */
1484 void collisionDetectBullets() {
1485 var i;
1486 // Collision detect player bullets with asteroids and enemies.
1487 for (i = 0; i < playerBullets.length; i++) {
1488 var bullet = playerBullets[i];
1489 var bulletRadius = bullet.radius;
1490 var bulletPos = bullet.position;
1491
1492 // Test circle intersection with each enemy actor.
1493 var n, m = enemies.length, z;
1494 for (n = 0; n < m; n++) {
1495 var enemy = enemies[n];
1496
1497 // Test the distance against the two radius combined.
1498 if (bulletPos.distance(enemy.position) <= bulletRadius + enemy.radius){
1499 // intersection detected!
1500
1501 // Test for area effect bomb weapon.
1502 var effectRad = bullet.effectRadius;
1503 if (effectRad == 0) {
1504 // Impact the enemy with the bullet.
1505 if (enemy.hit(bullet.power)) {
1506 // Destroy the enemy under the bullet.
1507 destroyEnemy(enemy, bullet.velocity, true);
1508 // Randomly release a power up.
1509 generatePowerUp(enemy);
1510 } else {
1511 // Add a bullet impact particle effect to show the hit.
1512 var effect =
1513 new PlayerBulletImpact(bullet.position, bullet.velocity);
1514 effects.add(effect);
1515 }
1516 } else {
1517 // Inform enemy it has been hit by a instant kill weapon.
1518 enemy.hit(-1);
1519 generatePowerUp(enemy);
1520
1521 // Add a big explosion actor at the area weapon position and vector.
1522 var comboCount = 1;
1523 var boom = new Explosion(
1524 bullet.position.clone(),
1525 bullet.velocity.nscale(0.5), 5);
1526 effects.add(boom);
1527
1528 // Destroy the enemy.
1529 destroyEnemy(enemy, bullet.velocity, true);
1530
1531 // Wipe out nearby enemies under the weapon effect radius
1532 // take the length of the enemy actor list here - so we don't
1533 // kill off -all- baby asteroids - so some elements of the original
1534 // survive.
1535 for (var x = 0, z = this.enemies.length, e; x < z; x++) {
1536 e = enemies[x];
1537
1538 // test the distance against the two radius combined
1539 if (bulletPos.distance(e.position) <= effectRad + e.radius) {
1540 e.hit(-1);
1541 generatePowerUp(e);
1542 destroyEnemy(e, bullet.velocity, true);
1543 comboCount++;
1544 }
1545 }
1546
1547 // Special score and indicator for "combo" detonation.
1548 if (comboCount > 4) {
1549 // Score bonus based on combo size.
1550 var inc = comboCount * 1000 * wave;
1551 game.score += inc;
1552
1553 // Generate a special effect indicator at the destroyed
1554 // enemy position.
1555 var vec = new Vector(0, -3.0);
1556 var effect = new ScoreIndicator(
1557 new Vector(enemy.position.x,
1558 enemy.position.y - (enemy.size * 8)),
1559 vec.add(enemy.velocity.nscale(0.5)),
1560 inc, 16, 'COMBO X ${comboCount}', 'rgb(255,255,55)', 1000);
1561 effects.add(effect);
1562
1563 // Generate a powerup to reward the player for the combo.
1564 generatePowerUp(enemy, true);
1565 }
1566 }
1567
1568 // Remove this bullet from the actor list as it has been destroyed.
1569 playerBullets.removeAt(i);
1570 break;
1571 }
1572 }
1573 }
1574
1575 // collision detect enemy bullets with asteroids
1576 for (i = 0; i < enemyBullets.length; i++) {
1577 var bullet = enemyBullets[i];
1578 var bulletRadius = bullet.radius;
1579 var bulletPos = bullet.position;
1580
1581 // test circle intersection with each enemy actor
1582 var n, m = enemies.length, z;
1583 for (n = 0; n < m; n++) {
1584 var enemy = enemies[n];
1585
1586 if (enemy is Asteroid) {
1587 if (bulletPos.distance(enemy.position) <=
1588 bulletRadius + enemy.radius) {
1589 // Impact the enemy with the bullet.
1590 if (enemy.hit(1)) {
1591 // Destroy the enemy under the bullet.
1592 destroyEnemy(enemy, bullet.velocity, false);
1593 } else {
1594 // Add a bullet impact particle effect to show the hit.
1595 var effect = new EnemyBulletImpact(bullet.position,
1596 bullet.velocity);
1597 effects.add(effect);
1598 }
1599
1600 // Remove this bullet from the actor list as it has been destroyed.
1601 enemyBullets.removeAt(i);
1602 break;
1603 }
1604 }
1605 }
1606 }
1607 }
1608
1609 /** Randomly generate a power up to reward the player */
1610 void generatePowerUp(EnemyActor enemy, [bool force = false]) {
1611 if (collectables.length < 5 &&
1612 (force || randomInt(0, ((enemy is Asteroid) ? 25 : 1)) == 0)) {
1613 // Apply a small random vector in the direction of travel
1614 // rotate by slightly randomized enemy heading.
1615 var vec = enemy.velocity.clone();
1616 var t = new Vector(0.0, -(random() * 2));
1617 t.rotate(enemy.velocity.theta() * (random() * Math.PI));
1618 vec.add(t);
1619
1620 // Add a power up to the collectables list.
1621 collectables.add(new PowerUp(
1622 new Vector(enemy.position.x, enemy.position.y - (enemy.size * 8)) ,
1623 vec));
1624 }
1625 }
1626
1627 /**
1628 * Blow up an enemy.
1629 *
1630 * An asteroid may generate new baby asteroids and leave an explosion
1631 * in the wake.
1632 *
1633 * Also applies the score for the destroyed item.
1634 *
1635 * @param enemy {Game.EnemyActor} The enemy to destory and add score for
1636 * @param parentVector {Vector} The vector of the item that hit the enemy
1637 * @param player {boolean} If true, the player was the destroyer
1638 */
1639 void destroyEnemy(EnemyActor enemy, Vector parentVector, player) {
1640 if (enemy is Asteroid) {
1641 soundManager.play('asteroid_boom${randomInt(1,4)}');
1642
1643 // generate baby asteroids
1644 generateBabyAsteroids(enemy, parentVector);
1645
1646 // add an explosion at the asteriod position and vector
1647 var boom = new AsteroidExplosion(
1648 enemy.position.clone(), enemy.velocity.clone(), enemy);
1649 effects.add(boom);
1650
1651 if (player!= null) {
1652 // increment score based on asteroid size
1653 var inc = ((5 - enemy.size) * 4) * 100 * wave;
1654 game.score += inc;
1655
1656 // generate a score effect indicator at the destroyed enemy position
1657 var vec = new Vector(0, -1.5).add(enemy.velocity.nscale(0.5));
1658 var effect = new ScoreIndicator(
1659 new Vector(enemy.position.x, enemy.position.y -
1660 (enemy.size * 8)), vec, inc);
1661 effects.add(effect);
1662 }
1663 } else if (enemy is EnemyShip) {
1664 soundManager.play('asteroid_boom1');
1665
1666 // add an explosion at the enemy ship position and vector
1667 var boom = new EnemyExplosion(enemy.position.clone(),
1668 enemy.velocity.clone(), enemy);
1669 effects.add(boom);
1670
1671 if (player != null) {
1672 // increment score based on asteroid size
1673 var inc = 2000 * wave * (enemy.size + 1);
1674 game.score += inc;
1675
1676 // generate a score effect indicator at the destroyed enemy position
1677 var vec = new Vector(0, -1.5).add(enemy.velocity.nscale(0.5));
1678 var effect = new ScoreIndicator(
1679 new Vector(enemy.position.x, enemy.position.y - 16),
1680 vec, inc);
1681 effects.add(effect);
1682 }
1683
1684 // decrement scene ship count
1685 enemyShipCount--;
1686 }
1687 }
1688
1689 /**
1690 * Generate a number of baby asteroids from a detonated parent asteroid.
1691 * The number and size of the generated asteroids are based on the parent
1692 * size. Some of the momentum of the parent vector (e.g. impacting bullet)
1693 * is applied to the new asteroids.
1694 */
1695 void generateBabyAsteroids(Asteroid asteroid, Vector parentVector) {
1696 // generate some baby asteroid(s) if bigger than the minimum size
1697 if (asteroid.size > 1) {
1698 var xc=randomInt(asteroid.size ~/ 2, asteroid.size - 1);
1699 for (var x=0; x < xc; x++) {
1700 var babySize = randomInt(1, asteroid.size - 1);
1701
1702 var vec = asteroid.velocity.clone();
1703
1704 // apply a small random vector in the direction of travel
1705 var t = new Vector(0.0, -random());
1706
1707 // rotate vector by asteroid current heading - slightly randomized
1708 t.rotate(asteroid.velocity.theta() * (random() * Math.PI));
1709 vec.add(t);
1710
1711 // add the scaled parent vector - to give some momentum from the impact
1712 vec.add(parentVector.nscale(0.2));
1713
1714 // create the asteroid - slightly offset from the centre of the old one
1715 var baby = new Asteroid(
1716 new Vector(asteroid.position.x + (random()*5)-2.5,
1717 asteroid.position.y + (random()*5)-2.5),
1718 vec, babySize, asteroid.type);
1719 enemies.add(baby);
1720 }
1721 }
1722 }
1723
1724 /** Render each actor to the canvas. */
1725 void renderActors(CanvasRenderingContext2D ctx){
1726 for (var i = 0, j = actors.length; i < j; i++) {
1727 // walk each sub-list and call render on each object
1728 var actorList = actors[i];
1729
1730 for (var n = actorList.length - 1; n >= 0; n--) {
1731 actorList[n].onRender(ctx);
1732 }
1733 }
1734 }
1735
1736 /**
1737 * DEBUG - Render the radius of the collision detection circle around
1738 * each actor.
1739 */
1740 void renderCollisionRadius(CanvasRenderingContext2D ctx) {
1741 ctx.save();
1742 ctx.strokeStyle = "rgb(255,0,0)";
1743 ctx.lineWidth = 0.5;
1744 ctx.shadowBlur = 0;
1745
1746 for (var i = 0, j = actors.length; i < j; i++) {
1747 var actorList = actors[i];
1748
1749 for (var n = actorList.length - 1, actor; n >= 0; n--) {
1750 actor = actorList[n];
1751 if (actor.radius) {
1752 ctx.beginPath();
1753 ctx.arc(actor.position.x, actor.position.y, actor.radius, 0,
1754 TWOPI, true);
1755 ctx.closePath();
1756 ctx.stroke();
1757 }
1758 }
1759 }
1760 ctx.restore();
1761 }
1762
1763 /**
1764 * Render player information HUD overlay graphics.
1765 *
1766 * @param ctx {object} Canvas rendering context
1767 */
1768 void renderOverlay(CanvasRenderingContext2D ctx) {
1769 ctx.save();
1770 ctx.shadowBlur = 0;
1771
1772 // energy bar (100 pixels across, scaled down from player energy max)
1773 ctx.strokeStyle = "rgb(50,50,255)";
1774 ctx.strokeRect(4, 4, 101, 6);
1775 ctx.fillStyle = "rgb(100,100,255)";
1776 var energy = player.energy;
1777 if (energy > player.ENERGY_INIT) {
1778 // the shield is on for "free" briefly when he player respawns
1779 energy = player.ENERGY_INIT;
1780 }
1781 ctx.fillRect(5, 5, (energy / (player.ENERGY_INIT / 100)), 5);
1782
1783 // lives indicator graphics
1784 for (var i=0; i<game.lives; i++) {
1785 drawScaledImage(ctx, g_playerImg, 0, 0, 64,
1786 350+(i*20), 0, 16);
1787
1788 // score display - update towards the score in increments to animate it
1789 var score = game.score;
1790 var inc = (score - scoredisplay) ~/ 10;
1791 scoredisplay += inc;
1792 if (scoredisplay > score) {
1793 scoredisplay = score;
1794 }
1795 var sscore = scoredisplay.ceil().toString();
1796 // pad with zeros
1797 for (var i=0, j=8-sscore.length; i<j; i++) {
1798 sscore = "0${sscore}";
1799 }
1800 fillText(ctx, sscore, "12pt Courier New", 120, 12, "white");
1801
1802 // high score
1803 // TODO: add method for incrementing score so this is not done here
1804 if (score > game.highscore) {
1805 game.highscore = score;
1806 }
1807 sscore = game.highscore.toString();
1808 // pad with zeros
1809 for (var i=0, j=8-sscore.length; i<j; i++) {
1810 sscore = "0${sscore}";
1811 }
1812 fillText(ctx, "HI: ${sscore}", "12pt Courier New", 220, 12, "white");
1813
1814 // debug output
1815 if (DEBUG['fps']) {
1816 fillText(ctx, "FPS: ${GameHandler.maxfps}", "12pt Courier New",
1817 0, GameHandler.height - 2, "lightblue");
1818 }
1819 }
1820 ctx.restore();
1821 }
1822 }
1823
1824
1825
1826 class Interval {
1827 String label;
1828 Function intervalRenderer;
1829 int framecounter = 0;
1830 bool complete = false;
1831
1832 Interval([this.label = null, this.intervalRenderer = null]);
1833
1834 void reset() {
1835 framecounter = 0;
1836 complete = false;
1837 }
1838 }
1839
1840 class Bullet extends ShortLivedActor {
1841
1842 Bullet(Vector position, Vector velocity,
1843 [this.heading = 0.0, int lifespan = 1300])
1844 : super(position, velocity, lifespan) {
1845 }
1846
1847 const int BULLET_WIDTH = 2;
1848 const int BULLET_HEIGHT = 6;
1849 const int FADE_LENGTH = 200;
1850
1851 double heading;
1852 int _power = 1;
1853
1854 void onRender(CanvasRenderingContext2D ctx) {
1855 // hack to stop draw under player graphic
1856 if (frameStart - start > 40) {
1857 ctx.save();
1858 ctx.globalCompositeOperation = "lighter";
1859 ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
1860 // rotate the bullet bitmap into the correct heading
1861 ctx.translate(position.x, position.y);
1862 ctx.rotate(heading * RAD);
1863 // TODO(gram) - figure out how to get rid of the vector art so we don't
1864 // need the [0] below.
1865 ctx.drawImage(GameHandler.bitmaps.images["bullet"],
1866 -(BULLET_WIDTH + GLOWSHADOWBLUR*2)*0.5,
1867 -(BULLET_HEIGHT + GLOWSHADOWBLUR*2)*0.5);
1868 ctx.restore();
1869 }
1870 }
1871
1872 /** Area effect weapon radius - zero for primary bullets. */
1873 get effectRadius => 0;
1874
1875 // approximate based on average between width and height
1876 get radius => 4;
1877
1878 get power => _power;
1879 }
1880
1881 /**
1882 * Player BulletX2 actor class. Used by the TwinCannons primary weapon.
1883 */
1884 class BulletX2 extends Bullet {
1885
1886 BulletX2(Vector position, Vector vector, double heading)
1887 : super(position, vector, heading, 1750) {
1888 _power = 2;
1889 }
1890
1891 void onRender(CanvasRenderingContext2D ctx) {
1892 // hack to stop draw under player graphic
1893 if (frameStart - start > 40) {
1894 ctx.save();
1895 ctx.globalCompositeOperation = "lighter";
1896 ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
1897 // rotate the bullet bitmap into the correct heading
1898 ctx.translate(position.x, position.y);
1899 ctx.rotate(heading * RAD);
1900 ctx.drawImage(GameHandler.bitmaps.images["bulletx2"],
1901 -(BULLET_WIDTH + GLOWSHADOWBLUR*4) / 2,
1902 -(BULLET_HEIGHT + GLOWSHADOWBLUR*2) / 2);
1903 ctx.restore();
1904 }
1905 }
1906
1907 get radius => BULLET_HEIGHT;
1908 }
1909
1910 class Bomb extends Bullet {
1911 Bomb(Vector position, Vector velocity)
1912 : super(position, velocity, 0.0, 3000);
1913
1914 const double BOMB_RADIUS = 4.0;
1915 const int FADE_LENGTH = 200;
1916 const int EFFECT_RADIUS = 45;
1917
1918 void onRender(CanvasRenderingContext2D ctx) {
1919 ctx.save();
1920 ctx.globalCompositeOperation = "lighter";
1921 ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
1922 ctx.translate(position.x, position.y);
1923 ctx.rotate((frameStart % (360*32)) / 32);
1924 var scale = fadeValue(1.0, FADE_LENGTH);
1925 if (scale <= 0) scale = 0.01;
1926 ctx.scale(scale, scale);
1927 ctx.drawImage(GameHandler.bitmaps.images["bomb"],
1928 -(BOMB_RADIUS + GLOWSHADOWBLUR),
1929 -(BOMB_RADIUS + GLOWSHADOWBLUR));
1930 ctx.restore();
1931 }
1932
1933 get effectRadius => EFFECT_RADIUS;
1934 get radius => fadeValue(BOMB_RADIUS, FADE_LENGTH);
1935 }
1936
1937 class EnemyBullet extends Bullet {
1938 EnemyBullet(Vector position, Vector velocity)
1939 : super(position, velocity, 0.0, 2800);
1940
1941 const double BULLET_RADIUS = 4.0;
1942 const int FADE_LENGTH = 200;
vsm 2013/04/01 14:00:17 You can drop double and int above - obvious from d
1943
1944 void onRender(CanvasRenderingContext2D ctx) {
1945 ctx.save();
1946 ctx.globalAlpha = fadeValue(1.0, FADE_LENGTH);
1947 ctx.globalCompositeOperation = "lighter";
1948 ctx.translate(position.x, position.y);
1949 ctx.rotate((frameStart % (360*64)) / 64);
1950 var scale = fadeValue(1.0, FADE_LENGTH);
1951 if (scale <= 0) scale = 0.01;
1952 ctx.scale(scale, scale);
1953 ctx.drawImage(GameHandler.bitmaps.images["enemybullet"],
1954 -(BULLET_RADIUS + GLOWSHADOWBLUR),
1955 -(BULLET_RADIUS + GLOWSHADOWBLUR));
1956 ctx.restore();
1957 }
1958
1959 get radius => fadeValue(BULLET_RADIUS, FADE_LENGTH) + 1;
1960 }
1961
1962 class Particle extends ShortLivedActor {
1963 int size;
1964 int type;
1965 int fadelength;
1966 String colour;
vsm 2013/04/01 14:00:17 color. :-)
1967 double rotate;
1968 double rotationv;
1969
1970 Particle(Vector position, Vector velocity, this.size, this.type,
1971 int lifespan, this.fadelength,
1972 [this.colour = Colours.PARTICLE])
1973 : super(position, velocity, lifespan) {
1974
1975 // randomize rotation speed and angle for line particle
1976 if (type == 1) {
1977 rotate = random() * TWOPI;
1978 rotationv = (random() - 0.5) * 0.5;
1979 }
1980 }
1981
1982 bool update() {
1983 position.add(velocity);
1984 return !expired();
1985 }
1986
1987 void render(CanvasRenderingContext2D ctx) {
1988 ctx.globalAlpha = fadeValue(1.0, fadelength);
1989 switch (type) {
1990 case 0: // point (prerendered image)
1991 ctx.translate(position.x, position.y);
1992 ctx.drawImage(
1993 GameHandler.bitmaps.images["points_${colour}"][size], 0, 0);
1994 break;
1995 // TODO: prerender a glowing line to use as the particle!
1996 case 1: // line
1997 ctx.translate(position.x, position.y);
1998 var s = size;
1999 ctx.rotate(rotate);
2000 this.rotate += rotationv;
2001 ctx.strokeStyle = colour;
2002 ctx.lineWidth = 1.5;
2003 ctx.beginPath();
2004 ctx.moveTo(-s, -s);
2005 ctx.lineTo(s, s);
2006 ctx.closePath();
2007 ctx.stroke();
2008 break;
2009 case 2: // smudge (prerendered image)
2010 var offset = (size + 1) << 2;
2011 renderImage(ctx,
2012 GameHandler.bitmaps.images["smudges_${colour}"][size],
2013 0, 0, (size + 1) << 3,
2014 position.x - offset, position.y - offset, (size + 1) << 3);
2015 break;
2016 }
2017 }
2018 }
2019
2020 /**
2021 * Particle emitter effect actor class.
2022 *
2023 * A simple particle emitter, that does not recycle particles, but sets itself
2024 * as expired() once all child particles have expired.
2025 *
2026 * Requires a function known as the emitter that is called per particle
2027 * generated.
2028 */
2029 class ParticleEmitter extends Actor {
2030
2031 List<Particle> particles;
2032
2033 ParticleEmitter(Vector position, Vector velocity)
2034 : super(position, velocity);
2035
2036 Particle emitter() {}
2037
2038 void init(count) {
2039 // generate particles based on the supplied emitter function
2040 particles = [];
2041 for (var i = 0; i < count; i++) {
2042 particles.add(emitter());
2043 }
2044 }
2045
2046 void onRender(CanvasRenderingContext2D ctx) {
2047 ctx.save();
2048 ctx.shadowBlur = 0;
2049 ctx.globalCompositeOperation = "lighter";
2050 for (var i=0, particle; i < particles.length; i++) {
2051 particle = particles[i];
2052
2053 // update particle and test for lifespan
2054 if (particle.update()) {
2055 ctx.save();
2056 particle.render(ctx);
2057 ctx.restore();
2058 } else {
2059 // particle no longer alive, remove from list
2060 particles.removeAt(i);
2061 }
2062 }
2063 ctx.restore();
2064 }
2065
2066 bool expired() => (particles.length == 0);
2067 }
2068
2069 class AsteroidExplosion extends ParticleEmitter {
2070 var asteroid;
2071
2072 AsteroidExplosion(Vector position, Vector vector, this.asteroid)
2073 : super(position, vector) {
2074 init(asteroid.size*2);
2075 }
2076
2077 Particle emitter() {
2078 // Randomise radial direction vector - speed and angle, then add parent
2079 // vector.
2080 var pos = position.clone();
2081 if (random() < 0.5) {
2082 var t = new Vector(0, randomInt(5, 10));
2083 t.rotate(random() * TWOPI).add(velocity);
2084 return new Particle(pos, t, (random() * 4).floor(), 0, 400, 300);
2085 } else {
2086 var t = new Vector(0, randomInt(1, 3));
2087 t.rotate(random() * TWOPI).add(velocity);
2088 return new Particle(pos, t,
2089 (random() * 4).floor() + asteroid.size, 2, 500, 250);
2090 }
2091 }
2092 }
2093
2094 class PlayerExplosion extends ParticleEmitter {
2095 PlayerExplosion(Vector position, Vector vector)
2096 : super(position, vector) {
2097 init(12);
2098 }
2099
2100 Particle emitter() {
2101 // Randomise radial direction vector - speed and angle, then add
2102 // parent vector.
2103 var pos = position.clone();
2104 if (random() < 0.5){
2105 var t = new Vector(0, randomInt(5, 10));
2106 t.rotate(random() * TWOPI).add(velocity);
2107 return new Particle(pos, t, (random() * 4).floor(), 0, 400, 300);
2108 } else {
2109 var t = new Vector(0, randomInt(1, 3));
2110 t.rotate(random() * TWOPI).add(velocity);
2111 return new Particle(pos, t, (random() * 4).floor() + 2, 2, 500, 250);
2112 }
2113 }
2114 }
2115
2116 /** Enemy particle based explosion - Particle effect actor class. */
2117 class EnemyExplosion extends ParticleEmitter {
2118 var enemy;
2119 EnemyExplosion(Vector position, Vector vector, this.enemy)
2120 : super(position, vector) {
2121 init(8);
2122 }
2123
2124 Particle emitter() {
2125 // randomise radial direction vector - speed and angle, then
2126 // add parent vector.
2127 var pos = position.clone();
2128 if (random() < 0.5) {
2129 var t = new Vector(0, randomInt(5, 10));
2130 t.rotate(random() * TWOPI).add(velocity);
2131 return new Particle(pos, t, (random() * 4).floor(), 0,
2132 400, 300, Colours.ENEMY_SHIP);
2133 } else {
2134 var t = new Vector(0, randomInt(1, 3));
2135 t.rotate(random() * 2 * TWOPI).add(velocity);
2136 return new Particle(pos, t,
2137 (random() * 4).floor() + (enemy.size == 0 ? 2 : 0), 2,
2138 500, 250, Colours.ENEMY_SHIP);
2139 }
2140 }
2141 }
2142
2143 class Explosion extends EffectActor {
2144 /**
2145 * Basic explosion effect actor class.
2146 *
2147 * TODO: replace all instances of this with particle effects
2148 * - this is still usedby the smartbomb
2149 */
2150 Explosion(Vector position, Vector vector, this.size)
2151 : super(position, vector, FADE_LENGTH);
2152
2153 static const int FADE_LENGTH = 300;
vsm 2013/04/01 14:00:17 drop int
2154
2155 num size = 0;
2156
2157 void onRender(CanvasRenderingContext2D ctx) {
2158 // fade out
2159 var brightness = (effectValue(255.0)).floor(),
2160 rad = effectValue(size * 8.0),
2161 rgb = brightness.toString();
2162 ctx.save();
2163 ctx.globalAlpha = 0.75;
2164 ctx.fillStyle = "rgb(${rgb},0,0)";
2165 ctx.beginPath();
2166 ctx.arc(position.x, position.y, rad, 0, TWOPI, true);
2167 ctx.closePath();
2168 ctx.fill();
2169 ctx.restore();
2170 }
2171 }
2172
2173 /**
2174 * Player bullet impact effect - Particle effect actor class.
2175 * Used when an enemy is hit by player bullet but not destroyed.
2176 */
2177 class PlayerBulletImpact extends ParticleEmitter {
2178 PlayerBulletImpact(Vector position, Vector vector)
2179 : super(position, vector) {
2180 init(5);
2181 }
2182
2183 Particle emitter() {
2184 // slightly randomise vector angle - then add parent vector
2185 var t = velocity.nscale(0.75 + random() * 0.5);
2186 t.rotate(random() * PIO4 - PIO8);
2187 return new Particle(position.clone(), t,
2188 (random() * 4).floor(), 0, 250, 150, Colours.GREEN_LASER);
2189 }
2190 }
2191
2192 /**
2193 * Enemy bullet impact effect - Particle effect actor class.
2194 * Used when an enemy is hit by player bullet but not destroyed.
2195 */
2196 class EnemyBulletImpact extends ParticleEmitter {
2197 EnemyBulletImpact(Vector position , Vector vector)
2198 : super(position, vector) {
2199 init(5);
2200 }
2201
2202 Particle emitter() {
2203 // slightly randomise vector angle - then add parent vector
2204 var t = velocity.nscale(0.75 + random() * 0.5);
2205 t.rotate(random() * PIO4 - PIO8);
2206 return new Particle(position.clone(), t,
2207 (random() * 4).floor(), 0, 250, 150, Colours.ENEMY_SHIP);
2208 }
2209 }
2210
2211
2212 class Player extends SpriteActor {
2213 Player(Vector position, Vector vector, this.heading)
2214 : super(position, vector) {
2215 energy = ENERGY_INIT;
2216
2217 // setup SpriteActor values - used for shield sprite
2218 animImage = g_shieldImg;
2219 animLength = SHIELD_ANIM_LENGTH;
2220
2221 // setup weapons
2222 primaryWeapons = {};
2223 }
2224
2225 double MAX_PLAYER_VELOCITY = 8.0;
vsm 2013/04/01 14:00:17 Mark all these const or lowercase.
2226 num PLAYER_RADIUS = 9;
2227 num SHIELD_RADIUS = 14;
2228 num SHIELD_ANIM_LENGTH = 100;
2229 num SHIELD_MIN_PULSE = 20;
2230 num ENERGY_INIT = 400;
2231 num THRUST_DELAY_MS = 100;
2232 num BOMB_RECHARGE_MS = 800;
2233 num BOMB_ENERGY = 80;
2234
2235 double heading = 0.0;
2236
2237 /** Player energy (shield and bombs). */
2238 num energy = 0;
2239
2240 /** Player shield active counter. */
2241 num shieldCounter = 0;
2242
2243 bool alive = true;
2244 Map primaryWeapons = null;
2245
2246 /** Bomb fire recharging counter. */
2247 num bombRecharge = 0;
2248
2249 /** Engine thrust recharge counter. */
2250 num thrustRecharge = 0;
2251
2252 /** True if the engine thrust graphics should be rendered next frame. */
2253 bool engineThrust = false;
2254
2255 /**
2256 * Time that the player was killed - to cause a delay before respawning
2257 * the player
2258 */
2259 num killedOn = 0;
2260
2261 bool fireWhenShield = false;
2262
2263 /** Player rendering method
2264 *
2265 * @param ctx {object} Canvas rendering context
2266 */
2267 void onRender(CanvasRenderingContext2D ctx) {
2268 var headingRad = heading * RAD;
2269
2270 // render engine thrust?
2271 if (engineThrust) {
2272 ctx.save();
2273 ctx.translate(position.x, position.y);
2274 ctx.rotate(headingRad);
2275 ctx.globalAlpha = 0.5 + random() * 0.5;
2276 ctx.globalCompositeOperation = "lighter";
2277 ctx.fillStyle = Colours.PLAYER_THRUST;
2278 ctx.beginPath();
2279 ctx.moveTo(-5, 8);
2280 ctx.lineTo(5, 8);
2281 ctx.lineTo(0, 18 + random() * 6);
2282 ctx.closePath();
2283 ctx.fill();
2284 ctx.restore();
2285 engineThrust = false;
2286 }
2287
2288 // render player graphic
2289 var size = (PLAYER_RADIUS * 2) + 6;
2290 // normalise the player heading to 0-359 degrees
2291 // then locate the correct frame in the sprite strip -
2292 // an image for each 4 degrees of rotation
2293 var normAngle = heading.floor() % 360;
2294 if (normAngle < 0) {
2295 normAngle = 360 + normAngle;
2296 }
2297 ctx.save();
2298 drawScaledImage(ctx, g_playerImg,
2299 0, (normAngle / 4).floor() * 64, 64,
2300 position.x - (size / 2), position.y - (size / 2), size);
2301 ctx.restore();
2302
2303 // shield up? if so render a shield graphic around the ship
2304 if (shieldCounter > 0 && energy > 0) {
2305 // render shield graphic bitmap
2306 ctx.save();
2307 ctx.translate(position.x, position.y);
2308 ctx.rotate(headingRad);
2309 renderSprite(ctx, -SHIELD_RADIUS-1,
2310 -SHIELD_RADIUS-1, (SHIELD_RADIUS * 2) + 2);
2311 ctx.restore();
2312
2313 shieldCounter--;
2314 energy -= 1.5;
2315 }
2316 }
2317
2318 /** Execute player forward thrust request. */
2319 void thrust() {
2320 // now test we did not thrust too recently, based on time since last thrust
2321 // request - ensures same thrust at any framerate
2322 if (frameStart - thrustRecharge > THRUST_DELAY_MS) {
2323 // update last thrust time
2324 thrustRecharge = frameStart;
2325
2326 // generate a small thrust vector
2327 var t = new Vector(0.0, -0.5);
2328
2329 // rotate thrust vector by player current heading
2330 t.rotate(heading * RAD);
2331
2332 // add player thrust vector to position
2333 velocity.add(t);
2334
2335 // player can't exceed maximum velocity - scale vector down if
2336 // this occurs - do this rather than not adding the thrust at all
2337 // otherwise the player cannot turn and thrust at max velocity
2338 if (velocity.length() > MAX_PLAYER_VELOCITY) {
2339 velocity.scale(MAX_PLAYER_VELOCITY / velocity.length());
2340 }
2341 }
2342 // mark so that we know to render engine thrust graphics
2343 engineThrust = true;
2344 }
2345
2346 /**
2347 * Execute player active shield request.
2348 * If energy remaining the shield will be briefly applied.
2349 */
2350 void activateShield() {
2351 // ensure shield stays up for a brief pulse between key presses!
2352 if (energy >= SHIELD_MIN_PULSE) {
2353 shieldCounter = SHIELD_MIN_PULSE;
2354 }
2355 }
2356
2357 bool isShieldActive() => (shieldCounter > 0 && energy > 0);
2358
2359 get radius => (isShieldActive() ? SHIELD_RADIUS : PLAYER_RADIUS);
2360
2361 bool expired() => !(alive);
2362
2363 void kill() {
2364 alive = false;
2365 killedOn = frameStart;
2366 }
2367
2368 /** Fire primary weapon(s). */
2369
2370 void firePrimary(List bulletList) {
2371 var playedSound = false;
2372 // attempt to fire the primary weapon(s)
2373 // first ensure player is alive and the shield is not up
2374 if (alive && (!isShieldActive() || fireWhenShield)) {
2375 for (var w in primaryWeapons.keys) {
2376 var b = primaryWeapons[w].fire();
2377 if (b != null) {
2378 for (var i=0; i<b.length; i++) {
2379 bulletList.add(b[i]);
2380 }
2381 if (!playedSound) {
2382 soundManager.play('laser');
2383 playedSound = true;
2384 }
2385 }
2386 }
2387 }
2388 }
2389
2390 /**
2391 * Fire secondary weapon.
2392 * @param bulletList {Array} to add bullet to on success
2393 */
2394 void fireSecondary(List bulletList) {
2395 // Attempt to fire the secondary weapon and generate bomb object if
2396 // successful. First ensure player is alive and the shield is not up.
2397 if (alive && (!isShieldActive() || fireWhenShield) && energy > BOMB_ENERGY){
2398 // now test we did not fire too recently
2399 if (frameStart - bombRecharge > BOMB_RECHARGE_MS) {
2400 // ok, update last fired time and we can now generate a bomb
2401 bombRecharge = frameStart;
2402
2403 // decrement energy supply
2404 energy -= BOMB_ENERGY;
2405
2406 // generate a vector rotated to the player heading and then add the
2407 // current player vector to give the bomb the correct directional
2408 // momentum.
2409 var t = new Vector(0.0, -3.0);
2410 t.rotate(heading * RAD);
2411 t.add(velocity);
2412
2413 bulletList.add(new Bomb(position.clone(), t));
2414 }
2415 }
2416 }
2417
2418 void onUpdate(_) {
2419 // slowly recharge the shield - if not active
2420 if (!isShieldActive() && energy < ENERGY_INIT) {
2421 energy += 0.1;
2422 }
2423 }
2424
2425 void reset(bool persistPowerUps) {
2426 // reset energy, alive status, weapons and power up flags
2427 alive = true;
2428 if (!persistPowerUps) {
2429 primaryWeapons = {};
2430 primaryWeapons["main"] = new PrimaryWeapon(this);
2431 fireWhenShield = false;
2432 }
2433 energy = ENERGY_INIT + SHIELD_MIN_PULSE; // for shield as below
2434
2435 // active shield briefly
2436 activateShield();
2437 }
2438 }
2439
2440
2441
2442
2443 /**
2444 * Image Preloader class. Executes the supplied callback function once all
2445 * registered images are loaded by the browser.
2446 */
2447 class Preloader {
2448 Preloader() {
2449 images = new List();
2450 }
2451
2452 /**
2453 * Image list
2454 *
2455 * @property images
2456 * @type Array
2457 */
2458 var images = [];
2459
2460 /**
2461 * Callback function
2462 *
2463 * @property callback
2464 * @type Function
2465 */
2466 var callback = null;
2467
2468 /**
2469 * Images loaded so far counter
2470 */
2471 var counter = 0;
2472
2473 /**
2474 * Add an image to the list of images to wait for
2475 */
2476 void addImage(ImageElement img, String url) {
2477 var me = this;
2478 img.src = url;
2479 // attach closure to the image onload handler
2480 img.onLoad.listen((_) {
2481 me.counter++;
2482 if (me.counter == me.images.length) {
2483 // all images are loaded - execute callback function
2484 me.callback();
2485 }
2486 });
2487 images.add(img);
2488 }
2489
2490 /**
2491 * Load the images and call the supplied function when ready
2492 */
2493 void onLoadCallback(Function fn) {
2494 counter = 0;
2495 callback = fn;
2496 // load the images
2497 //for (var i=0, j = images.length; i<j; i++) {
2498 // images[i].src = images[i].url;
2499 //}
2500 }
2501 }
2502
2503
2504
2505 /**
2506 * Game prerenderer class.
2507 */
2508 class GamePrerenderer {
2509 GamePrerenderer();
2510
2511 /**
2512 * Image list. Keyed by renderer ID - returning an array also. So to get
2513 * the first image output by prerenderer with id "default":
2514 * images["default"][0]
2515 */
2516 Map images = {};
2517 Map _renderers = {};
2518
2519 /** Add a renderer function to the list of renderers to execute. */
2520 addRenderer(Function fn, String id) => _renderers[id] = fn;
2521
2522
2523 /** Execute all prerender functions. */
2524 void execute() {
2525 var buffer = new CanvasElement();
2526 for (var id in _renderers.keys) {
2527 images[id] = _renderers[id](buffer);
2528 }
2529 }
2530 }
2531
2532 /**
2533 * Asteroids prerenderer class.
2534 *
2535 * Encapsulates the early rendering of various effects used in the game. Each
2536 * effect is rendered once to a hidden canvas object, the image data is
2537 * extracted and stored in an Image object - which can then be reused later.
2538 * This is much faster than rendering each effect again and again at runtime.
2539 *
2540 * The downside to this is that some constants are duplicated here and in the
2541 * original classes - so updates to the original classes such as the weapon
2542 * effects must be duplicated here.
2543 */
2544 class Prerenderer extends GamePrerenderer {
2545 Prerenderer() : super() {
2546
2547 // function to generate a set of point particle images
2548 var fnPointRenderer = (CanvasElement buffer, String colour) {
2549 var imgs = [];
2550 for (var size = 3; size <= 6; size++) {
2551 var width = size << 1;
2552 buffer.width = buffer.height = width;
2553 CanvasRenderingContext2D ctx = buffer.getContext('2d');
2554 var radgrad = ctx.createRadialGradient(size, size, size >> 1,
2555 size, size, size);
2556 radgrad.addColorStop(0, colour);
2557 radgrad.addColorStop(1, "#000");
2558 ctx.fillStyle = radgrad;
2559 ctx.fillRect(0, 0, width, width);
2560 var img = new ImageElement();
2561 img.src = buffer.toDataUrl("image/png");
2562 imgs.add(img);
2563 }
2564 return imgs;
2565 };
2566
2567 // add the various point particle image prerenderers based on above function
2568 // default explosion colour
2569 addRenderer((CanvasElement buffer) {
2570 return fnPointRenderer(buffer, Colours.PARTICLE);
2571 }, "points_${Colours.PARTICLE}");
2572
2573 // player bullet impact particles
2574 addRenderer((CanvasElement buffer) {
2575 return fnPointRenderer(buffer, Colours.GREEN_LASER);
2576 }, "points_${Colours.GREEN_LASER}");
2577
2578 // enemy bullet impact particles
2579 addRenderer((CanvasElement buffer) {
2580 return fnPointRenderer(buffer, Colours.ENEMY_SHIP);
2581 }, "points_${Colours.ENEMY_SHIP}");
2582
2583 // add the smudge explosion particle image prerenderer
2584 var fnSmudgeRenderer = (CanvasElement buffer, String colour) {
2585 var imgs = [];
2586 for (var size = 4; size <= 32; size += 4) {
2587 var width = size << 1;
2588 buffer.width = buffer.height = width;
2589 CanvasRenderingContext2D ctx = buffer.getContext('2d');
2590 var radgrad = ctx.createRadialGradient(size, size, size >> 3,
2591 size, size, size);
2592 radgrad.addColorStop(0, colour);
2593 radgrad.addColorStop(1, "#000");
2594 ctx.fillStyle = radgrad;
2595 ctx.fillRect(0, 0, width, width);
2596 var img = new ImageElement();
2597 img.src = buffer.toDataUrl("image/png");
2598 imgs.add(img);
2599 }
2600 return imgs;
2601 };
2602
2603 addRenderer((CanvasElement buffer) {
2604 return fnSmudgeRenderer(buffer, Colours.PARTICLE);
2605 }, "smudges_${Colours.PARTICLE}");
2606
2607 addRenderer((CanvasElement buffer) {
2608 return fnSmudgeRenderer(buffer, Colours.ENEMY_SHIP);
2609 }, "smudges_${Colours.ENEMY_SHIP}");
2610
2611 // standard player bullet
2612 addRenderer((CanvasElement buffer) {
2613 // NOTE: keep in sync with Asteroids.Bullet
2614 var BULLET_WIDTH = 2, BULLET_HEIGHT = 6;
2615 var imgs = [];
2616 buffer.width = BULLET_WIDTH + GLOWSHADOWBLUR*2;
2617 buffer.height = BULLET_HEIGHT + GLOWSHADOWBLUR*2;
2618 CanvasRenderingContext2D ctx = buffer.getContext('2d');
2619
2620 var rf = (width, height) {
2621 ctx.beginPath();
2622 ctx.moveTo(0, height);
2623 ctx.lineTo(width, 0);
2624 ctx.lineTo(0, -height);
2625 ctx.lineTo(-width, 0);
2626 ctx.closePath();
2627 };
2628
2629 ctx.shadowBlur = GLOWSHADOWBLUR;
2630 ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
2631 ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASER_DARK;
2632 rf(BULLET_WIDTH-1, BULLET_HEIGHT-1);
2633 ctx.fill();
2634 ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASER;
2635 rf(BULLET_WIDTH, BULLET_HEIGHT);
2636 ctx.fill();
2637 var img = new ImageElement();
2638 img.src = buffer.toDataUrl("image/png");
2639 return img;
2640 }, "bullet");
2641
2642 // player bullet X2
2643 addRenderer((CanvasElement buffer) {
2644 // NOTE: keep in sync with Asteroids.BulletX2
2645 var BULLET_WIDTH = 2, BULLET_HEIGHT = 6;
2646 buffer.width = BULLET_WIDTH + GLOWSHADOWBLUR*4;
2647 buffer.height = BULLET_HEIGHT + GLOWSHADOWBLUR*2;
2648 CanvasRenderingContext2D ctx = buffer.getContext('2d');
2649
2650 var rf = (width, height) {
2651 ctx.beginPath();
2652 ctx.moveTo(0, height);
2653 ctx.lineTo(width, 0);
2654 ctx.lineTo(0, -height);
2655 ctx.lineTo(-width, 0);
2656 ctx.closePath();
2657 };
2658
2659 ctx.shadowBlur = GLOWSHADOWBLUR;
2660 ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
2661 ctx.save();
2662 ctx.translate(-4, 0);
2663 ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASERX2_DARK;
2664 rf(BULLET_WIDTH-1, BULLET_HEIGHT-1);
2665 ctx.fill();
2666 ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASERX2;
2667 rf(BULLET_WIDTH, BULLET_HEIGHT);
2668 ctx.fill();
2669 ctx.translate(8, 0);
2670 ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASERX2_DARK;
2671 rf(BULLET_WIDTH-1, BULLET_HEIGHT-1);
2672 ctx.fill();
2673 ctx.shadowColor = ctx.fillStyle = Colours.GREEN_LASERX2;
2674 rf(BULLET_WIDTH, BULLET_HEIGHT);
2675 ctx.fill();
2676 ctx.restore();
2677 var img = new ImageElement();
2678 img.src = buffer.toDataUrl("image/png");
2679 return img;
2680 }, "bulletx2");
2681
2682 // player bomb weapon
2683 addRenderer((CanvasElement buffer) {
2684 // NOTE: keep in sync with Asteroids.Bomb
2685 var BOMB_RADIUS = 4;
2686 buffer.width = buffer.height = BOMB_RADIUS*2 + GLOWSHADOWBLUR*2;
2687 CanvasRenderingContext2D ctx = buffer.getContext('2d');
2688
2689 var rf = () {
2690 ctx.beginPath();
2691 ctx.moveTo(BOMB_RADIUS * 2, 0);
2692 for (var i = 0; i < 15; i++) {
2693 ctx.rotate(PIO8);
2694 if (i % 2 == 0) {
2695 ctx.lineTo((BOMB_RADIUS * 2 / 0.525731) * 0.200811, 0);
2696 } else {
2697 ctx.lineTo(BOMB_RADIUS * 2, 0);
2698 }
2699 }
2700 ctx.closePath();
2701 };
2702
2703 ctx.shadowBlur = GLOWSHADOWBLUR;
2704 ctx.shadowColor = ctx.fillStyle = Colours.PLAYER_BOMB;
2705 ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
2706 rf();
2707 ctx.fill();
2708
2709 var img = new ImageElement();
2710 img.src = buffer.toDataUrl("image/png");
2711 return img;
2712 }, "bomb");
2713
2714 //enemy weapon
2715 addRenderer((CanvasElement buffer) {
2716 // NOTE: keep in sync with Asteroids.EnemyBullet
2717 var BULLET_RADIUS = 4;
2718 var imgs = [];
2719 buffer.width = buffer.height = BULLET_RADIUS*2 + GLOWSHADOWBLUR*2;
2720 CanvasRenderingContext2D ctx = buffer.getContext('2d');
2721
2722 var rf = () {
2723 ctx.beginPath();
2724 ctx.moveTo(BULLET_RADIUS * 2, 0);
2725 for (var i=0; i<7; i++) {
2726 ctx.rotate(PIO4);
2727 if (i % 2 == 0) {
2728 ctx.lineTo((BULLET_RADIUS * 2/0.525731) * 0.200811, 0);
2729 } else {
2730 ctx.lineTo(BULLET_RADIUS * 2, 0);
2731 }
2732 }
2733 ctx.closePath();
2734 };
2735
2736 ctx.shadowBlur = GLOWSHADOWBLUR;
2737 ctx.shadowColor = ctx.fillStyle = Colours.ENEMY_SHIP;
2738 ctx.translate(buffer.width * 0.5, buffer.height * 0.5);
2739 ctx.beginPath();
2740 ctx.arc(0, 0, BULLET_RADIUS-1, 0, TWOPI, true);
2741 ctx.closePath();
2742 ctx.fill();
2743 rf();
2744 ctx.fill();
2745
2746 var img = new ImageElement();
2747 img.src = buffer.toDataUrl("image/png");
2748 return img;
2749 }, "enemybullet");
2750 }
2751 }
2752
2753 /**
2754 * Game scene base class.
2755 */
2756 class Scene {
2757 bool playable;
2758 Interval interval;
2759
2760 Scene([this.playable = true, this.interval = null]);
2761
2762 /** Return true if this scene should update the actor list. */
2763 bool isPlayable() => playable;
2764
2765 void onInitScene() {
2766 if (interval != null) {
2767 // reset interval flag
2768 interval.reset();
2769 }
2770 }
2771
2772 void onBeforeRenderScene() {}
2773 void onRenderScene(ctx) {}
2774 void onRenderInterval(ctx) {}
2775 void onMouseDownHandler(e) {}
2776 void onMouseUpHandler(e) {}
2777 void onKeyDownHandler(int keyCode) {}
2778 void onKeyUpHandler(int keyCode) {}
2779 bool isComplete() => false;
2780
2781 bool onAccelerometer(double x, double y, double z) {
2782 return true;
2783 }
2784 }
2785
2786 class SoundManager {
2787
2788 Map sounds = {};
2789
2790 void createSound(Map props) {
2791 var a = new AudioElement();
2792 a.volume = props['volume'] / 100.0;;
2793 a.src = props['url'];
2794 sounds[props['id']] = a;
2795 }
2796
2797 void play(String id) {
2798 sounds[id].play();
2799 }
2800 }
2801
2802 /**
2803 * An actor that can be rendered by a bitmap. The sprite handling code deals
2804 * with the increment of the current frame within the supplied bitmap sprite
2805 * strip image, based on animation direction, animation speed and the animation
2806 * length before looping. Call renderSprite() each frame.
2807 *
2808 * NOTE: by default sprites source images are 64px wide 64px by N frames high
2809 * and scaled to the appropriate final size. Any other size input source should
2810 * be set in the constructor.
2811 */
2812 class SpriteActor extends Actor {
2813 SpriteActor(Vector position, Vector vector, [this.frameSize = 64])
2814 : super(position, vector);
2815
2816 /** Size in pixels of the width/height of an individual frame in the image. */
2817 int frameSize;
2818
2819 /**
2820 * Animation image sprite reference.
2821 * Sprite image sources are all currently 64px wide 64px by N frames high.
2822 */
2823 ImageElement animImage = null;
2824
2825 /** Length in frames of the sprite animation. */
2826 int animLength = 0;
2827
2828 /** Animation direction, true for forward, false for reverse. */
2829 bool animForward = true;
2830
2831 /** Animation frame inc/dec speed. */
2832 double animSpeed = 1.0;
2833
2834 /** Current animation frame index. */
2835 int animFrame = 0;
2836
2837 /**
2838 * Render sprite graphic based on current anim image, frame and anim direction
2839 * Automatically updates the current anim frame.
2840 */
2841 void renderSprite(CanvasRenderingContext2D ctx, num x, num y, num s) {
2842 renderImage(ctx, animImage, 0, animFrame << 6, frameSize, x, y, s);
2843
2844 // update animation frame index
2845 if (animForward) {
2846 animFrame += (animSpeed * frameMultiplier).toInt();
2847 if (animFrame >= animLength) {
2848 animFrame = 0;
2849 }
2850 } else {
2851 animFrame -= (animSpeed * frameMultiplier).toInt();
2852 if (animFrame < 0) {
2853 animFrame = animLength - 1;
2854 }
2855 }
2856 }
2857 }
2858
2859 class Star {
2860 Star();
2861
2862 double MAXZ = 12.0;
2863 double VELOCITY = 0.85;
2864
2865 num x = 0;
2866 num y = 0;
2867 num z = 0;
2868 num prevx = 0;
2869 num prevy = 0;
2870
2871 void init() {
2872 // select a random point for the initial location
2873 prevx = prevy = 0;
2874 x = (random() * GameHandler.width - (GameHandler.width * 0.5)) * MAXZ;
2875 y = (random() * GameHandler.height - (GameHandler.height * 0.5)) * MAXZ;
2876 z = MAXZ;
2877 }
2878
2879 void render(CanvasRenderingContext2D ctx) {
2880 var xx = x / z;
2881 var yy = y / z;
2882
2883 if (prevx != 0) {
2884 ctx.lineWidth = 1.0 / z * 5 + 1;
2885 ctx.beginPath();
2886 ctx.moveTo(prevx + (GameHandler.width * 0.5),
2887 prevy + (GameHandler.height * 0.5));
2888 ctx.lineTo(xx + (GameHandler.width * 0.5),
2889 yy + (GameHandler.height * 0.5));
2890 ctx.stroke();
2891 }
2892
2893 prevx = xx;
2894 prevy = yy;
2895 }
2896 }
2897
2898 void drawText(CanvasRenderingContext2D g,
2899 String txt, String font, num x, num y,
2900 [String color]) {
2901 g.save();
2902 if (color != null) g.strokeStyle = color;
2903 g.font = font;
2904 g.strokeText(txt, x, y);
2905 g.restore();
2906 }
2907
2908 void centerDrawText(CanvasRenderingContext2D g, String txt, String font, num y,
2909 [String color]) {
2910 g.save();
2911 if (color != null) g.strokeStyle = color;
2912 g.font = font;
2913 g.strokeText(txt, (GameHandler.width - g.measureText(txt).width) / 2, y);
2914 g.restore();
2915 }
2916
2917 void fillText(CanvasRenderingContext2D g, String txt, String font, num x, num y,
2918 [String color]) {
2919 g.save();
2920 if (color != null) g.fillStyle = color;
2921 g.font = font;
2922 g.fillText(txt, x, y);
2923 g.restore();
2924 }
2925
2926 void centerFillText(CanvasRenderingContext2D g, String txt, String font, num y,
2927 [String color]) {
2928 g.save();
2929 if (color != null) g.fillStyle = color;
2930 g.font = font;
2931 g.fillText(txt, (GameHandler.width - g.measureText(txt).width) / 2, y);
2932 g.restore();
2933 }
2934
2935 void drawScaledImage(CanvasRenderingContext2D ctx, ImageElement image,
2936 num nx, num ny, num ns, num x, num y, num s) {
2937 ctx.drawImageToRect(image, new Rect(x, y, s, s),
2938 sourceRect: new Rect(nx, ny, ns, ns));
2939 }
2940 /**
2941 * This method will automatically correct for objects moving on/off
2942 * a cyclic canvas play area - if so it will render the appropriate stencil
2943 * sections of the sprite top/bottom/left/right as needed to complete the image.
2944 * Note that this feature can only be used if the sprite is absolutely
2945 * positioned and not translated/rotated into position by canvas operations.
2946 */
2947 void renderImage(CanvasRenderingContext2D ctx, ImageElement image,
2948 num nx, num ny, num ns, num x, num y, num s) {
2949 print("renderImage(_,$nx,$ny,$ns,$ns,$x,$y,$s,$s)");
2950 ctx.drawImageScaledFromSource(image, nx, ny, ns, ns, x, y, s, s);
2951
2952 if (x < 0) {
2953 ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
2954 GameHandler.width + x, y, s, s);
2955 }
2956 if (y < 0) {
2957 ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
2958 x, GameHandler.height + y, s, s);
2959 }
2960 if (x < 0 && y < 0) {
2961 ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
2962 GameHandler.width + x, GameHandler.height + y, s, s);
2963 }
2964 if (x + s > GameHandler.width) {
2965 ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
2966 x - GameHandler.width, y, s, s);
2967 }
2968 if (y + s > GameHandler.height) {
2969 ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
2970 x, y - GameHandler.height, s, s);
2971 }
2972 if (x + s > GameHandler.width && y + s > GameHandler.height) {
2973 ctx.drawImageScaledFromSource(image, nx, ny, ns, ns,
2974 x - GameHandler.width, y - GameHandler.height, s, s);
2975 }
2976 }
2977
2978 void renderImageRotated(CanvasRenderingContext2D ctx, ImageElement image,
2979 num x, num y, num w, num h, num r) {
2980 var w2 = w*0.5, h2 = h*0.5;
2981 var rf = (tx, ty) {
2982 ctx.save();
2983 ctx.translate(tx, ty);
2984 ctx.rotate(r);
2985 ctx.drawImage(image, -w2, -h2);
2986 ctx.restore();
2987 };
2988
2989 rf(x, y);
2990
2991 if (x - w2 < 0) {
2992 rf(GameHandler.width + x, y);
2993 }
2994 if (y - h2 < 0) {
2995 rf(x, GameHandler.height + y);
2996 }
2997 if (x - w2 < 0 && y - h2 < 0) {
2998 rf(GameHandler.width + x, GameHandler.height + y);
2999 }
3000 if (x - w2 + w > GameHandler.width) {
3001 rf(x - GameHandler.width, y);
3002 }
3003 if (y - h2 + h > GameHandler.height){
3004 rf(x, y - GameHandler.height);
3005 }
3006 if (x - w2 + w > GameHandler.width && y - h2 + h > GameHandler.height) {
3007 rf(x - GameHandler.width, y - GameHandler.height);
3008 }
3009 }
3010
3011 void renderImageRotated2(CanvasRenderingContext2D ctx, ImageElement image,
3012 num x, num y, num w, num h, num r) {
3013 print("Rendering rotated sprite ${image.src} to dest $x,$y");
3014 var w2 = w*0.5, h2 = h*0.5;
3015 var rf = (tx, ty) {
3016 ctx.save();
3017 ctx.translate(tx, ty);
3018 ctx.rotate(r);
3019 ctx.drawImage(image, -w2, -h2);
3020 ctx.restore();
3021 };
3022
3023 rf(x, y);
3024
3025 if (x - w2 < 0) {
3026 rf(GameHandler.width + x, y);
3027 }
3028 if (y - h2 < 0) {
3029 rf(x, GameHandler.height + y);
3030 }
3031 if (x - w2 < 0 && y - h2 < 0) {
3032 rf(GameHandler.width + x, GameHandler.height + y);
3033 }
3034 if (x - w2 + w > GameHandler.width) {
3035 rf(x - GameHandler.width, y);
3036 }
3037 if (y - h2 + h > GameHandler.height){
3038 rf(x, y - GameHandler.height);
3039 }
3040 if (x - w2 + w > GameHandler.width && y - h2 + h > GameHandler.height) {
3041 rf(x - GameHandler.width, y - GameHandler.height);
3042 }
3043 }
3044
3045 class Vector {
3046 num x, y;
3047
3048 Vector(this.x, this.y);
3049
3050 Vector clone() => new Vector(x, y);
3051
3052 void set(Vector v) {
3053 x = v.x;
3054 y = v.y;
3055 }
3056
3057 Vector add(Vector v) {
3058 x += v.x;
3059 y += v.y;
3060 return this;
3061 }
3062
3063 Vector nadd(Vector v) => new Vector(x + v.x, y + v.y);
3064
3065 Vector sub(Vector v) {
3066 x -= v.x;
3067 y -= v.y;
3068 return this;
3069 }
3070
3071 Vector nsub(Vector v) => new Vector(x - v.x, y - v.y);
3072
3073 double dot(Vector v) => x * v.x + y * v.y;
3074
3075 double length() => Math.sqrt(x * x + y * y);
3076
3077 double distance(Vector v) {
3078 var dx = x - v.x;
3079 var dy = y - v.y;
3080 return Math.sqrt(dx * dx + dy * dy);
3081 }
3082
3083 double theta() => Math.atan2(y, x);
3084
3085 double thetaTo(Vector vec) {
3086 // calc angle between the two vectors
3087 var v = clone().norm();
3088 var w = vec.clone().norm();
3089 return Math.sqrt(v.dot(w));
3090 }
3091
3092 double thetaTo2(Vector vec) =>
3093 Math.atan2(vec.y, vec.x) - Math.atan2(y, x);
3094
3095 Vector norm() {
3096 var len = length();
3097 x /= len;
3098 y /= len;
3099 return this;
3100 }
3101
3102 Vector nnorm() {
3103 var len = length();
3104 return new Vector(x / len, y / len);
3105 }
3106
3107 rotate(num a) {
3108 var ca = Math.cos(a);
3109 var sa = Math.sin(a);
3110 var newx = x*ca - y*sa;
3111 var newy = x*sa + y*ca;
3112 x = newx;
3113 y = newy;
3114 return this;
3115 }
3116
3117 Vector nrotate(num a) {
3118 var ca = Math.cos(a);
3119 var sa = Math.sin(a);
3120 return new Vector(x * ca - y * sa, x * sa + y * ca);
3121 }
3122
3123 Vector invert() {
3124 x = -x;
3125 y = -y;
3126 return this;
3127 }
3128
3129 Vector ninvert() {
3130 return new Vector(-x, -y);
3131 }
3132
3133 Vector scale(num s) {
3134 x *= s;
3135 y *= s;
3136 return this;
3137 }
3138
3139 Vector nscale(num s) {
3140 return new Vector(x * s, y * s);
3141 }
3142
3143 Vector scaleTo(num s) {
3144 var len = s / length();
3145 x *= len;
3146 y *= len;
3147 return this;
3148 }
3149
3150 nscaleTo(num s) {
3151 var len = s / length();
3152 return new Vector(x * len, y * len);
3153 }
3154
3155 trim(num minx, num maxx, num miny, num maxy) {
3156 if (x < minx) x = minx;
3157 else if (x > maxx) x = maxx;
3158 if (y < miny) y = miny;
3159 else if (y > maxy) y = maxy;
3160 }
3161
3162 wrap(num minx, num maxx, num miny, num maxy) {
3163 if (x < minx) x = maxx;
3164 else if (x > maxx) x = minx;
3165 if (y < miny) y = maxy;
3166 else if (y > maxy) y = miny;
3167 }
3168
3169 String toString() => "<$x, $y>";
3170 }
3171
3172 class Weapon {
3173 Weapon(this.player, [this.rechargeTime = 125]);
3174
3175 int rechargeTime;
3176 int lastFired = 0;
3177 Player player;
3178
3179 bool canFire() =>
3180 (GameHandler.frameStart - lastFired) >= rechargeTime;
3181
3182 List fire() {
3183 if (canFire()) {
3184 lastFired = GameHandler.frameStart;
3185 return doFire();
3186 }
3187 }
3188
3189 Bullet makeBullet(double headingDelta, double vectorY,
3190 [int lifespan = 1300]) {
3191 var h = player.heading - headingDelta;
3192 var t = new Vector(0.0, vectorY).rotate(h * RAD).add(player.velocity);
3193 return new Bullet(player.position.clone(), t, h, lifespan);
3194 }
3195
3196 List doFire() => [];
3197 }
3198
3199 class PrimaryWeapon extends Weapon {
3200 PrimaryWeapon(Player player) : super(player);
3201
3202 List doFire() => [ makeBullet(0.0, -4.5) ];
3203 }
3204
3205 class TwinCannonsWeapon extends Weapon {
3206 TwinCannonsWeapon(Player player) : super(player, 150);
3207
3208 List doFire() {
3209 var h = player.heading;
3210 var t = new Vector(0.0, -4.5).rotate(h * RAD).add(player.velocity);
3211 return [ new BulletX2(player.position.clone(), t, h) ];
3212 }
3213 }
3214
3215 class VSprayCannonsWeapon extends Weapon {
3216 VSprayCannonsWeapon(Player player) : super(player, 250);
3217
3218 List doFire() =>
3219 [ makeBullet(-15.0, -3.75),
3220 makeBullet(0.0, -3.75),
3221 makeBullet(15.0, -3.75) ];
3222 }
3223
3224 class SideGunWeapon extends Weapon {
3225 SideGunWeapon(Player player) : super(player, 250);
3226
3227 List doFire() =>
3228 [ makeBullet(-90.0, -4.5, 750),
3229 makeBullet(+90.0, -4.5, 750)];
3230 }
3231
3232 class RearGunWeapon extends Weapon {
3233 RearGunWeapon(Player player) : super(player, 250);
3234
3235 List doFire() => [makeBullet(180.0, -4.5, 750)];
3236 }
3237
3238 class Input {
3239 bool left, right, thrust, shield, fireA, fireB;
3240
3241 Input() { reset(); }
3242
3243 void reset() {
3244 left = right = thrust = shield = fireA = fireB = false;
3245 }
3246 }
3247
3248 void resize(int w, int h) {}
3249
3250
3251 void setup(canvasp, int w, int h) {
3252 var canvas;
3253 if (canvasp == null) {
3254 log("Allocating canvas");
3255 canvas = new CanvasElement(width: w, height: h);
3256 document.body.nodes.add(canvas);
3257 } else {
3258 log("Using parent canvas");
3259 canvas = canvasp;
3260 }
3261
3262 for (var i = 0; i < 4; i++) {
3263 g_asteroidImgs.add(new ImageElement());
3264 }
3265 // attach to the image onload handler
3266 // once the background is loaded, we can boot up the game
3267 g_backgroundImg.onLoad.listen((e) {
3268 // init our game with Game.Main derived instance
3269 log("Loaded background image ${g_backgroundImg.src}");
3270 GameHandler.init(canvas);
3271 GameHandler.start(new AsteroidsMain());
3272 });
3273 g_backgroundImg.src = 'bg3_1.png';
3274 loadSounds();
3275 }
3276
3277 void loadSounds() {
3278 soundManager = new SoundManager();
3279 // load game sounds
3280 soundManager.createSound({
3281 'id': 'laser',
3282 'url': 'laser.$sfx_extension',
3283 'volume': 40,
3284 'autoLoad': true,
3285 'multiShot': true
3286 });
3287 soundManager.createSound({
3288 'id': 'enemy_bomb',
3289 'url': 'enemybomb.$sfx_extension',
3290 'volume': 60,
3291 'autoLoad': true,
3292 'multiShot': true
3293 });
3294 soundManager.createSound({
3295 'id': 'big_boom',
3296 'url': 'bigboom.$sfx_extension',
3297 'volume': 50,
3298 'autoLoad': true,
3299 'multiShot': true
3300 });
3301 soundManager.createSound({
3302 'id': 'asteroid_boom1',
3303 'url': 'explosion1.$sfx_extension',
3304 'volume': 50,
3305 'autoLoad': true,
3306 'multiShot': true
3307 });
3308 soundManager.createSound({
3309 'id': 'asteroid_boom2',
3310 'url': 'explosion2.$sfx_extension',
3311 'volume': 50,
3312 'autoLoad': true,
3313 'multiShot': true
3314 });
3315 soundManager.createSound({
3316 'id': 'asteroid_boom3',
3317 'url': 'explosion3.$sfx_extension',
3318 'volume': 50,
3319 'autoLoad': true,
3320 'multiShot': true
3321 });
3322 soundManager.createSound({
3323 'id': 'asteroid_boom4',
3324 'url': 'explosion4.$sfx_extension',
3325 'volume': 50,
3326 'autoLoad': true,
3327 'multiShot': true
3328 });
3329 soundManager.createSound({
3330 'id': 'powerup',
3331 'url': 'powerup.$sfx_extension',
3332 'volume': 50,
3333 'autoLoad': true,
3334 'multiShot': true
3335 });
3336 }
3337
3338
3339
3340
3341
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698