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 |