1 /** 2 * Game objects and logic 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.demo.pewpew.objects; 15 16 import std.algorithm.comparison; 17 import std.random; 18 import std.math; 19 20 import ae.utils.container.listnode; 21 import ae.utils.math; 22 import ae.utils.geometry; 23 import ae.utils.graphics.color; 24 import ae.utils.graphics.draw; 25 import ae.utils.graphics.image; 26 27 __gshared: 28 29 enum Plane 30 { 31 Logic, 32 BG0, 33 BG1, 34 BG2, 35 Ship, 36 PlasmaOrbs, 37 Enemies, 38 Particles, 39 Torpedoes, 40 Explosions, 41 Max 42 } 43 44 enum STAR_LAYERS = 3; 45 46 DListContainer!GameEntity[Plane.Max] planes; 47 bool initializing = true; 48 49 alias L16 COLOR; 50 //alias G8 COLOR; // less precise, but a bit faster 51 52 Image!COLOR canvas; 53 float cf(float x) { assert(canvas.w == canvas.h); return x*canvas.w; } 54 int ci(float x) { assert(canvas.w == canvas.h); return cast(int)(x*canvas.w); } 55 T cbound(T)(T x) { return bound(x, 0, canvas.w); } 56 auto BLACK = COLOR(0); 57 auto WHITE = COLOR(COLOR.ChannelType.max); 58 59 bool up, down, left, right, fire; 60 61 bool useAnalog; float analogX, analogY; 62 63 float frand () { return uniform!`[)`( 0.0f, 1.0f); } 64 float frands() { return uniform!`()`(-1.0f, 1.0f); } 65 66 T ssqr(T)(T x) { return sqr(x) * sign(x); } 67 float frands2() { return ssqr(frands()); } 68 69 mixin FixMath; 70 71 enum Sound 72 { 73 fire, 74 warpIn, 75 torpedoHit, 76 enemyFire, 77 explosion, 78 } 79 Sound[] sounds; 80 81 int score; 82 83 // ********************************************************* 84 85 class GameEntity 86 { 87 mixin DListLink; 88 Plane plane; 89 90 abstract void step(uint deltaTicks); 91 abstract void render(); 92 93 void add(Plane plane) 94 { 95 this.plane = plane; 96 planes[plane].pushBack(this); 97 } 98 99 void remove() 100 { 101 planes[plane].remove(this); 102 } 103 } 104 105 class Game : GameEntity 106 { 107 this() 108 { 109 add(Plane.Logic); 110 spawnParticles = new SpawnParticles(Plane.Particles); 111 foreach (layer; 0..STAR_LAYERS) 112 starFields[layer] = new StarField(cast(Plane)(Plane.BG0 + layer)); 113 torpedoParticles = new TorpedoParticles(Plane.Particles); 114 } 115 116 uint spawnTimer; 117 118 override void step(uint deltaTicks) 119 { 120 foreach (i; 0..deltaTicks) 121 { 122 auto z = frand(); 123 auto star = Star( 124 frand(), 0, 125 0.0001f + (1-z) * 0.00005f, 126 COLOR(fixfpart(tofix((1-z)*0.5f)))); 127 auto layer = cast(int)((1-z)*3); 128 starFields[layer].add(star); 129 } 130 if (!initializing && ship && !ship.dead && spawnTimer--==0) 131 { 132 new Thingy(); 133 spawnTimer = uniform(1500, 2500); 134 } 135 if (!initializing && planes[Plane.Ship].empty && planes[Plane.Enemies].empty && planes[Plane.PlasmaOrbs].empty && planes[Plane.Explosions].empty) 136 new Ship(); 137 } 138 139 override void render() {} 140 } 141 142 // ********************************************************* 143 144 class ParticleManager(Particle) : GameEntity 145 { 146 Particle* particles; 147 int particleCount; 148 149 void add(Particle particle) 150 { 151 if (particleCount == Particle.MAX) 152 return; 153 particles[particleCount++] = particle; 154 } 155 156 this(Plane plane) 157 { 158 particles = (new Particle[Particle.MAX]).ptr; 159 super.add(plane); 160 } 161 162 override void step(uint deltaTicks) 163 { 164 int i = 0; 165 while (i < particleCount) 166 with (particles[i]) 167 { 168 enum REMOVE = q{ particles[i] = particles[--particleCount]; }; 169 enum NEXT = q{ i++; }; 170 mixin(Particle.STEP); 171 } 172 } 173 174 override void render() 175 { 176 import std.parallelism; 177 foreach (ref particle; taskPool.parallel(particles[0..particleCount])) 178 with (particle) 179 { 180 mixin(Particle.RENDER); 181 } 182 } 183 } 184 185 /+ 186 class ParticleManager(Particle) : GameEntity 187 { 188 Particle[] particles; 189 190 void add(Particle particle) 191 { 192 particles ~= particle; 193 } 194 195 this(Plane plane) 196 { 197 super.add(plane); 198 } 199 200 override void step(uint deltaTicks) 201 { 202 for (int i=0; i<particles.length; i++) 203 with (particles[i]) 204 { 205 enum REMOVE = q{ particles = particles[0..i] ~ particles[i+1..$]; }; 206 enum NEXT = q{ }; 207 mixin(Particle.STEP); 208 } 209 } 210 211 override void render() 212 { 213 foreach (ref particle; particles) 214 with (particle) 215 { 216 mixin(Particle.RENDER); 217 } 218 } 219 } 220 +/ 221 222 struct Star 223 { 224 float x, y, vy; 225 COLOR color; 226 227 enum MAX = 1024*32; 228 229 enum STEP = 230 q{ 231 y += deltaTicks * vy; 232 if (y > 1f) 233 { mixin(REMOVE); } 234 else 235 { mixin(NEXT); } 236 }; 237 238 enum RENDER = 239 q{ 240 canvas.aaPutPixel(cf(x), cf(y), color); 241 }; 242 } 243 244 alias ParticleManager!Star StarField; 245 StarField[STAR_LAYERS] starFields; 246 247 // ********************************************************* 248 249 class GameObject : GameEntity 250 { 251 float x, y, vx=0, vy=0; 252 Shape!float[] shapes; /// shape coordinates are relative to x,y 253 bool dead; 254 255 enum DEATHBRAKES = 0.998f; 256 257 override void step(uint deltaTicks) 258 { 259 x += vx*deltaTicks; 260 y += vy*deltaTicks; 261 if (dead) 262 vx *= DEATHBRAKES, 263 vy *= DEATHBRAKES; 264 } 265 266 final void collideWith(Plane[] planeIndices...) 267 { 268 assert(!dead); 269 270 foreach (plane; planeIndices) 271 foreach (obj; planes[plane]) 272 { 273 auto enemy = cast(GameObject) obj; 274 if (enemy && !enemy.dead) 275 foreach (shape1; shapes) 276 { 277 if (shape1.kind == ShapeKind.none) continue; 278 shape1.translate(x, y); 279 foreach (shape2; enemy.shapes) 280 { 281 if (shape2.kind == ShapeKind.none) continue; 282 shape2.translate(enemy.x, enemy.y); 283 if (intersects(shape1, shape2)) 284 { 285 die(); 286 enemy.die(); 287 return; 288 } 289 } 290 } 291 } 292 } 293 294 void die() 295 { 296 remove(); 297 } 298 } 299 300 // ********************************************************* 301 302 class Ship : GameObject 303 { 304 float death = 0f, spawn = 0f; 305 bool spawning; 306 uint t; 307 308 enum SPAWN_START = 0.3f; 309 enum SPAWN_END = 0.3f; 310 311 this() 312 { 313 x = 0.5f; 314 y = 0.85f; 315 vx = vy = 0; 316 shapes ~= shape(rect(-0.040f, -0.020f, -0.028f, +0.040f)); // left wing 317 shapes ~= shape(rect(+0.040f, -0.020f, +0.028f, +0.040f)); // right wing 318 shapes ~= shape(rect(-0.008f, -0.040f, +0.008f, +0.030f)); // center hull 319 shapes ~= shape(rect(-0.030f, +0.020f, +0.030f, +0.024f)); // bridge 320 shapes ~= shape(circle(0, +0.020f, 0.020f)); // round section 321 add(Plane.Ship); 322 ship = this; 323 dead = spawning = true; 324 sounds ~= Sound.warpIn; 325 score = 0; 326 } 327 328 override void step(uint deltaTicks) 329 { 330 if (!dead) 331 { 332 const a = 0.000_001f; 333 const maxv = 0.000_500f; 334 335 if (useAnalog) 336 vx = analogX * maxv, 337 vy = analogY * maxv; 338 else 339 { 340 if (left) 341 vx = bound(vx-a*deltaTicks, -maxv, 0); 342 else 343 if (right) 344 vx = bound(vx+a*deltaTicks, 0, maxv); 345 else 346 vx = 0; 347 348 if (up) 349 vy = bound(vy-a*deltaTicks, -maxv, 0); 350 else 351 if (down) 352 vy = bound(vy+a*deltaTicks, 0, maxv); 353 else 354 vy = 0; 355 } 356 357 t += deltaTicks; 358 } 359 super.step(deltaTicks); 360 361 if (!dead) 362 { 363 if (x<0.05f || x>0.95f) vx = 0; 364 if (y<0.05f || y>0.95f) vy = 0; 365 x = bound(x, 0.05f, 0.95f); 366 y = bound(y, 0.05f, 0.95f); 367 368 static bool wasFiring; 369 //fire = t % 250 == 0; 370 if (fire && !wasFiring) 371 { 372 new Torpedo(-0.034f, -0.020f); 373 new Torpedo(+0.034f, -0.020f); 374 sounds ~= Sound.fire; 375 } 376 wasFiring = !!fire; 377 378 collideWith(Plane.Enemies, Plane.PlasmaOrbs); 379 } 380 else 381 if (spawning) 382 { 383 spawn += 0.0005f; 384 385 if (spawn < SPAWN_START+1f) 386 foreach (n; 0..5) 387 { 388 float px = frands()*0.050f; 389 spawnParticles.add(SpawnParticle( 390 x + px*1.7f + frands ()*0.010f, 391 spawnY() + frands2()*0.050f, 392 x + px, 393 )); 394 } 395 396 if (spawn >= SPAWN_START+1f+SPAWN_END) 397 spawning = dead = false; 398 } 399 else 400 { 401 death += (1f/2475f); 402 if (death > 1f) 403 remove(); 404 } 405 } 406 407 override void die() 408 { 409 new Explosion(this, 0.150f); 410 dead = true; 411 } 412 413 override void render() 414 { 415 enum Gray25 = COLOR.ChannelType.max / 4; 416 enum Gray75 = COLOR.ChannelType.max / 4 * 3; 417 418 void drawRect(float x0, float y0, float x1, float y1, COLOR color) 419 { 420 canvas.aaFillRect(cf(x+x0), cf(y+y0), cf(x+x1), cf(y+y1), color); 421 } 422 423 void drawRect2(Rect!float r) 424 { 425 r.sort(); 426 drawRect(r.x0 , r.y0 , r.x1 , r.y1 , COLOR(Gray25)); 427 drawRect(r.x0+0.002f, r.y0+0.002f, r.x1-0.002f, r.y1-0.002f, COLOR(Gray75)); 428 } 429 430 void drawCircle(Circle!float c, COLOR color) 431 { 432 canvas.softCircle(cf(x+c.x), cf(y+c.y), cf(c.r*0.7f), cf(c.r), color); 433 } 434 435 436 void warp(float x, float y, float r) 437 { 438 auto bgx0 = cbound(ci(x-r)); 439 auto bgy0 = cbound(ci(y-r)); 440 auto bgx1 = cbound(ci(x+r)); 441 auto bgy1 = cbound(ci(y+r)); 442 auto window = canvas.crop(bgx0, bgy0, bgx1, bgy1); 443 static Image!COLOR bg; 444 window.copy(bg); 445 auto cx = ci(x)-bgx0; 446 auto cy = ci(y)-bgy0; 447 procedural!((x, y) 448 { 449 int dx = x-cx; 450 int dy = y-cy; 451 int sx = x; 452 int sy = y; 453 float f = dist(dx, dy) / cx; 454 if (f < 1f && f > 0f) 455 { 456 float f2 = (1-f)*sqrt(sqrt(f)) + f*f; 457 assert(f2 < 1f); 458 sx = cx + cast(int)(dx / f * f2); 459 sy = cy + cast(int)(dy / f * f2); 460 } 461 return bg.safeGet(sx, sy, COLOR(0)); 462 })(window.w, window.h).blitTo(window); 463 } 464 465 if (spawning) 466 { 467 enum R = 0.15f; 468 warp(x, spawnY(), R * sqrt(sin(spawn/(SPAWN_START+1f+SPAWN_END)*PI))); 469 } 470 471 static Image!COLOR bg; 472 int bgx0, bgyS; 473 if (spawning) 474 { 475 bgx0 = ci(x-0.050f); 476 bgyS = ci(spawnY()); 477 canvas.crop(bgx0, bgyS, ci(x+0.050f), ci(y+0.050f)).copy(bg); 478 } 479 480 drawRect2 (shapes[0].rect); 481 drawRect2 (shapes[1].rect); 482 drawRect2 (shapes[2].rect); 483 drawRect (shapes[3].rect.tupleof, COLOR(Gray25)); 484 drawCircle(shapes[4].circle , COLOR(Gray75)); 485 486 if (spawning) 487 bg.blitTo(canvas, bgx0, bgyS); 488 } 489 490 float spawnY() 491 { 492 return y-0.050f + (0.100f * bound(spawn-SPAWN_START, 0f, 1f)); 493 } 494 } 495 496 Ship ship; 497 498 struct SpawnParticle 499 { 500 float x0, y0, x1, t=0f; 501 502 enum MAX = 1024*32; 503 504 enum STEP = 505 q{ 506 t += 0.002f; 507 if (t > 1f) 508 { mixin(REMOVE); } 509 else 510 { mixin(NEXT); } 511 }; 512 513 enum RENDER = 514 q{ 515 float y1 = ship.spawnY(); 516 float tt0 = sqr(sqr(t)); 517 float tt1 = min(1, tt0+0.15f); 518 float lx0 = x0 + tt0*(x1-x0); 519 float ly0 = y0 + tt0*(y1-y0); 520 float lx1 = x0 + tt1*(x1-x0); 521 float ly1 = y0 + tt1*(y1-y0); 522 //canvas.aaPutPixel(cf(x), cf(y), WHITE, tofracBounded(tt)); 523 canvas.aaLine(cf(lx0), cf(ly0), cf(lx1), cf(ly1), WHITE, tofracBounded(sqr(tt0))); 524 }; 525 } 526 527 alias ParticleManager!SpawnParticle SpawnParticles; 528 SpawnParticles spawnParticles; 529 530 // ********************************************************* 531 532 class Torpedo : GameObject 533 { 534 this(float dx, float dy) 535 { 536 this.x = ship.x + dx; 537 this.y = ship.y + dy; 538 this.vx = ship.vx; 539 this.vy = ship.vy - 0.000_550f; 540 shapes ~= Shape!float(Point!float(0, 0)); 541 add(Plane.Torpedoes); 542 } 543 544 int t; 545 546 override void step(uint deltaTicks) 547 { 548 super.step(deltaTicks); 549 if (y < -0.25f || x < 0 || x > 1) 550 return remove(); 551 552 //if ((t+=deltaTicks) % 1 == 0) 553 torpedoParticles.add(TorpedoParticle( 554 x, y, 555 frands()*0.000_010f, 556 frand ()*0.000_100f + 0.000_200f)); 557 558 collideWith(Plane.Enemies, Plane.PlasmaOrbs); 559 } 560 561 override void die() 562 { 563 remove(); 564 foreach (n; 0..500) 565 { 566 auto a = uniform(0, TAU); 567 torpedoParticles.add(TorpedoParticle(x, y, 568 frand()*cos(a)*0.000_300f, 569 frand()*sin(a)*0.000_300f - 0.000_100f, 570 0.003f)); 571 } 572 sounds ~= Sound.torpedoHit; 573 score += 10; 574 } 575 576 override void render() 577 { 578 canvas.aaPutPixel(cf(x), cf(y), WHITE); 579 } 580 } 581 582 struct TorpedoParticle 583 { 584 float x, y, vx, vy, s = 0.001f, t = 0; 585 586 enum MAX = 1024*64; 587 588 enum STEP = 589 q{ 590 x += vx; 591 y += vy; 592 t += s; 593 if (t >= 1) 594 mixin(REMOVE); 595 else 596 mixin(NEXT); 597 }; 598 599 enum RENDER = 600 q{ 601 canvas.aaPutPixel(cf(x), cf(y), WHITE, tofracBounded(1-t)); 602 }; 603 } 604 605 alias ParticleManager!TorpedoParticle TorpedoParticles; 606 TorpedoParticles torpedoParticles; 607 608 // ********************************************************* 609 610 class Enemy : GameObject 611 { 612 } 613 614 class ThingyPart : Enemy 615 { 616 float death = 0f; 617 618 override void render() 619 { 620 float r1 = shapes[0].circle.r; 621 float r0 = r1*(2f/3f); 622 623 if (!dead) 624 canvas.softCircle(cf(x), cf(y), cf(r0), cf(r1), WHITE); 625 else 626 { 627 canvas.softCircle(cf(x), cf(y), cf(r0), cf(r1), COLOR(tofracBounded(1-death))); 628 canvas.softCircle(cf(x), cf(y), cf(r0*death), cf(r1*death), BLACK); 629 } 630 } 631 } 632 633 class Thingy : ThingyPart 634 { 635 float a, va; 636 ThingySatellite[] satellites; 637 int charge; // max is 2000 638 639 this() 640 { 641 this.x = uniform(0f, 1f); 642 this.y = -0.060f; 643 this.vx = frands()*0.0003f; 644 this.vy = 0.0002f+frand()*0.0003f; 645 shapes ~= shape(circle(0, 0, 0.030f)); 646 va = 0.002f * sign(frands()); 647 a = frand()*TAU; 648 charge = 0; 649 auto numSatellites = uniform!"[]"(2, 2+(ship.t / 10_000)); 650 651 //charge=int.min; x=0.25f;vx=0;vy=0.0001f; static int c=2; numSatellites=c++; 652 653 foreach (i; 0..numSatellites) 654 satellites ~= new ThingySatellite(); 655 656 add(Plane.Enemies); 657 } 658 659 override void step(uint deltaTicks) 660 { 661 for (int n=0; n<deltaTicks; n++) 662 { 663 if (x < 0.020f) 664 vx = max(vx, -vx); 665 if (x > 0.980f) 666 vx = min(vx, -vx); 667 super.step(1); 668 if (y > 1.060f) 669 { 670 foreach (s; satellites) 671 if (!s.dead) 672 s.remove(); 673 remove(); 674 return; 675 } 676 a += va; 677 678 if (!dead) 679 { 680 foreach (s; satellites) 681 if (!s.dead) 682 charge++; 683 684 while (charge >= 2000) 685 { 686 charge -= 2000; 687 if (ship && !ship.dead) 688 new PlasmaOrb(this); 689 } 690 } 691 else 692 { 693 death += 0.002f; 694 if (death > 1) 695 remove(); 696 } 697 } 698 699 uint level = 0; 700 uint lastLevelCount = 1; 701 void arrange(ThingySatellite[] satellites) 702 { 703 foreach (i, s; satellites) 704 { 705 auto sd = 0.040f + 0.025f*level + s.death*0.020f; // satellite distance 706 auto sa = a + TAU*i/satellites.length + level*(TAU/lastLevelCount/2); 707 s.x = x + sd*cos(sa); 708 s.y = y + sd*sin(sa); 709 } 710 level++; 711 lastLevelCount = cast(uint)satellites.length; 712 } 713 714 if (satellites.length <= 8) 715 arrange(satellites[ 0.. $]); 716 else 717 if (satellites.length <= 12) 718 { 719 arrange(satellites[ 0.. 6]); 720 arrange(satellites[ 6.. $]); 721 } 722 else 723 if (satellites.length <= 24) 724 { 725 arrange(satellites[ 0.. 8]); 726 arrange(satellites[ 8.. $]); 727 } 728 else 729 if (satellites.length <= 32) 730 { 731 arrange(satellites[ 0.. 8]); 732 arrange(satellites[ 8..16]); 733 arrange(satellites[16.. $]); 734 } 735 else 736 if (satellites.length <= 48) 737 { 738 arrange(satellites[ 0.. 8]); 739 arrange(satellites[ 8..24]); 740 arrange(satellites[24.. $]); 741 } 742 else 743 if (satellites.length <= 64) 744 { 745 arrange(satellites[ 0.. 8]); 746 arrange(satellites[ 8..24]); 747 arrange(satellites[24..40]); 748 arrange(satellites[40.. $]); 749 } 750 else 751 { 752 arrange(satellites[ 0.. 8]); 753 arrange(satellites[ 8..24]); 754 arrange(satellites[24..48]); 755 arrange(satellites[48.. $]); 756 } 757 } 758 759 override void die() 760 { 761 dead = true; 762 foreach (s; satellites) 763 s.dead = true; 764 new Explosion(this, 0.060f); 765 } 766 } 767 768 class ThingySatellite : ThingyPart 769 { 770 this() 771 { 772 shapes ~= shape(circle(0, 0, 0.014f)); 773 add(Plane.Enemies); 774 } 775 776 override void step(uint deltaTicks) 777 { 778 if (dead) 779 { 780 death += 0.002f; 781 if (death > 1) 782 remove(); 783 } 784 } 785 786 override void die() 787 { 788 dead = true; 789 } 790 } 791 792 class PlasmaOrb : Enemy 793 { 794 int t; 795 float death = 0f; 796 797 this(Enemy parent) 798 { 799 this.x = parent.x; 800 this.y = parent.y; 801 shapes ~= shape(circle(0, 0, 0.008f)); 802 t = 0; 803 vx = ship.x - parent.x; 804 vy = ship.y - parent.y; 805 float f = 0.0002f/dist(this.vx, this.vy); 806 vx *= f; 807 vy *= f; 808 add(Plane.PlasmaOrbs); 809 sounds ~= Sound.enemyFire; 810 } 811 812 override void step(uint deltaTicks) 813 { 814 super.step(deltaTicks); 815 if (x<0 || x>1 || y<0 || y>1) 816 return remove(); 817 if (!dead) 818 { 819 t += deltaTicks; 820 shapes[0].circle.r = 0.008f+0.002f*sin(t/100f); 821 } 822 else 823 { 824 shapes[0].circle.r += 0.000_050f; 825 death += 0.005f; 826 if (death >= 1f) 827 remove(); 828 } 829 } 830 831 override void die() 832 { 833 dead = true; 834 if (ship && !ship.dead) 835 score -= 5; // These are worth less 836 } 837 838 override void render() 839 { 840 auto r = shapes[0].circle.r; 841 auto brightness = 0.75f+0.25f*(-sin(t/100f)); 842 if (!dead) 843 canvas.softCircle(cf(x), cf(y), cf(r-0.003f), cf(r), COLOR(tofracBounded(brightness))); 844 else 845 { 846 brightness *= 1-(death/2); 847 canvas.softRing(cf(x), cf(y), cf(death*r), cf(average(r, death*r)), cf(r), COLOR(tofracBounded(brightness))); 848 } 849 } 850 } 851 852 // ********************************************************* 853 854 class Explosion : GameObject 855 { 856 float size, maxt; 857 int t; 858 859 this(GameObject source, float size) 860 { 861 this.x = source.x; 862 this.y = source.y; 863 this.vx = source.vx; 864 this.vy = source.vy; 865 this.size = size; 866 this.t = 0; 867 this.maxt = size*16500; 868 this.dead = true; 869 add(Plane.Explosions); 870 } 871 872 override void step(uint deltaTicks) 873 { 874 t += deltaTicks; 875 super.step(deltaTicks); 876 877 if (t>maxt) 878 remove; 879 else 880 if (frand() < size) 881 { 882 float tf = t/maxt; // time factor 883 float ex = x+tf*size*frands(); 884 float ey = y+tf*size*frands(); 885 float ed = dist(x-ex, y-ey); 886 if (ed>size) return; 887 float r = 888 frand() * // random factor 889 (1-ed/size); // distance factor 890 new Splode(ex, ey, size/3*r); 891 } 892 } 893 894 override void die() { assert(0); } 895 896 override void render() 897 { 898 } 899 } 900 901 class Splode : GameEntity 902 { 903 float x, y, r, cr; 904 bool growing; 905 static uint index; 906 907 this(float x, float y, float r) 908 { 909 this.x = x; 910 this.y = y; 911 this.r = r; 912 this.cr = 0; 913 this.growing = true; 914 add(Plane.Explosions); 915 if (index++ % 32 == 0) 916 sounds ~= Sound.explosion; 917 } 918 919 override void step(uint deltaTicks) 920 { 921 const ra = 0.000_020f; 922 if (growing) 923 { 924 cr += ra; 925 if (cr>=r) 926 growing = false; 927 } 928 else 929 { 930 cr -= ra; 931 if (cr<=0) 932 { 933 remove(); 934 return; 935 } 936 } 937 } 938 939 override void render() 940 { 941 //std.stdio.writeln([x, y, cr]); 942 canvas.softCircle(cf(x), cf(y), max(0f, cf(cr)-1.5f), cf(cr), COLOR(tofracBounded(cr/r))); 943 } 944 }