| 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 |