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