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