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 }