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