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

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

Powered by Google App Engine
This is Rietveld 408576698