OLD | NEW |
| (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 | |
OLD | NEW |