1 /**
2 	The main texit module.
3 */
4 module texit;
5 
6 public import std.datetime.systime, std.getopt, std.file, bindbc.sdl;
7 public import std.string : toStringz;
8 public import std.conv : to;
9 
10 // things bindbc-sdl is missing
11 extern(C) {
12 	struct SDL_FRect {
13 		float x, y, w, h;
14 	}
15 	int SDL_RenderFillRectF(SDL_Renderer* renderer, const(SDL_FRect*) rect);
16 	int SDL_RenderCopyF(SDL_Renderer* renderer, SDL_Texture* texture, const(SDL_Rect*) srcrect, const(SDL_FRect*) dstrect);
17 }
18 
19 /// A single tile in the world
20 struct Tile {
21 	float[3] bg = [0, 0, 0];	 /// bg color
22 	float[3] fg = [1, 1, 1];	 /// fg color
23 	char ch = ' '; /// character to draw
24 
25 	bool opEquals(Tile t) {
26 		return bg == t.bg && fg == t.fg && ch == t.ch;
27 	}
28 }
29 
30 
31 
32 /// Ease a value given an easing from https://easings.net/ and also easeLinear (which returns the given value)
33 pure nothrow float ease(string easing)(float x) {
34 	import std.math.trigonometry : sin, cos;
35 	import std.math.algebraic		: sqrt;
36 	import std.math.constants		: PI;
37 	enum float c1 = 1.70158;
38 	enum float c2 = c1*1.525;
39 	enum float c3 = c1+1;
40 	enum float n1 = 7.5625;
41 	enum float d1 = 2.75;
42 	static if(easing == "easeLinear")
43 		return x;
44 	else static if(easing == "easeInSine")
45 		return 1-cos((x*PI)/2);
46 	else static if(easing == "easeOutSine")
47 		return sin((x*PI)/2);
48 	else static if(easing == "easeInOutSine")
49 		return -(cos(PI*x)-1)/2;
50 	else static if(easing == "easeInCubic")
51 		return x^^3;
52 	else static if(easing == "easeOutCubic")
53 		return 1-(1-x)^^3;
54 	else static if(easing == "easeInOutCubic")
55 		return x < 0.5 
56 			? 4*x^^3
57 			: 1-((-2*x+2)^^3)/2;
58 	else static if(easing == "easeInQuint")
59 		return x^^5;
60 	else static if(easing == "easeOutQuint")
61 		return 1-(1-x)^^5;
62 	else static if(easing == "easeInOutQuint")
63 		return x < 0.5 
64 			? 16*x^^5
65 			: 1-((-2*x+2)^^5)/2;
66 	else static if(easing == "easeInCirc")
67 		return 1-sqrt(1-x^^2);
68 	else static if(easing == "easeOutCirc")
69 		return sqrt(1-(x-1)^^2);
70 	else static if(easing == "easeInOutCirc")
71 		return x < 0.5
72 			? (1-sqrt(1-(2*x)^^2))/2
73 			: (sqrt(1-(-2*x+1)^^2)+1)/2;
74 	else static if(easing == "easeInQuad")
75 		return x^^2;
76 	else static if(easing == "easeOutQuad")
77 		return 1-(1-x)^^2;
78 	else static if(easing == "easeInOutQuad")
79 		return x < 0.5
80 			? 2*x^^2
81 			: 1-((-2*x+2)^^2)/2;
82 	else static if(easing == "easeInQuart")
83 		return x^^4;
84 	else static if(easing == "easeOutQuart")
85 		return 1-(1-x)^^4;
86 	else static if(easing == "easeInOutQuart")
87 		return x < 0.5
88 			? 8*x^^4
89 			: 1-((-2*x+2)^^4)/2;
90 	else static if(easing == "easeInExpo")
91 		return x == 0
92 			? 0
93 			: 2^^(10*x-10);
94 	else static if(easing == "easeOutExpo")
95 		return x == 1
96 			? 1
97 			: 1-2^^(-10*x);
98 	else static if(easing == "easeInOutExpo")
99 		return x == 0
100 			? 0
101 			: x == 1
102 				? 1
103 				: x < 0.5
104 					? 2^^(20*x-10)/2
105 					: (2-2^^(-20*x+10))/2;
106 	else static if(easing == "easeInBack")
107 		return (c3*x^^3)-(c1*x^^2);
108 	else static if(easing == "easeOutBack")
109 		return 1+c3*(x-1)^^3+c1*(x-1)^^2;
110 	else static if(easing == "easeInOutBack")
111 		return x < 0.5
112 			? ((2*x)^^2*((c2+1)*2*x-c2))/2
113 			: ((2*x-2)^^2*((c2+1)*(x*2-2)+c2)+2)/2;
114 	else static if(easing == "easeInBounce")
115 		return 1-ease!"easeOutBounce"(1-x);
116 	else static if(easing == "easeOutBounce") {
117 		if(x < 1/d1)
118 			return n1*x^^2;
119 		else if(x < 2/d1)
120 			return n1*(x -= 1.5/d1)*x+0.75;
121 		else if(x < 2.5/d1)
122 			return n1*(x -= 2.25/d1)*x+0.9375;
123 		else
124 			return n1*(x-=2.625/d1)*x+0.984375;
125 	}
126 	else static if(easing == "easeInOutBounce")
127 		return x < 0.5
128 			? (1-ease!"easeOutBounce"(1-2*x)/2)
129 			: (1+ease!"easeOutBounce"(2*x-1))/2;
130 	else
131 		static assert(false, "Unknown easing "~easing);
132 }
133 
134 alias Easing = pure float function(float);
135 
136 /// Takes an easing and returns a function pointer to it
137 Easing easing(string s)() {
138 	return &(ease!s);
139 }
140 
141 /// Simple vector struct
142 struct Vector {
143 	float x = 0, y = 0, z = 0;
144 }
145 
146 /// Simple point struct
147 struct Point {
148 	int x = 0, y = 0;
149 }
150 
151 /// Simple color struct
152 struct Color {
153 	float r = 0, g = 0, b = 0, a = 0;
154 
155 	SDL_Color toSDL() {
156 		return SDL_Color(cast(ubyte)(r*255), cast(ubyte)(g*255), cast(ubyte)(b*255), cast(ubyte)(a*255));
157 	}
158 }
159 
160 Vector translation; /// How much to translate the screen by
161 float zoom = 1;		 /// How much to zoom in/out
162 	
163 /// An exception with SDL
164 class SDLException : Exception {
165 	this(string msg) {
166 		super(msg);
167 	}
168 }
169 
170 /// Any other exception caused by texit
171 class TexitException : Exception {
172 	this(string msg) {
173 		super(msg);
174 	}
175 }
176 
177 /// The main texit declaration
178 mixin template Texit(string charmap, 
179 		int charSize, float scale,
180 		int worldWidth, int worldHeight, 
181 		int width, int height, 
182 		string title) {
183 	// some constants so the user can access them
184 	enum WIDTH = width;
185 	enum HEIGHT = height;
186 	enum WORLD_WIDTH = worldWidth;
187 	alias WW = WORLD_WIDTH;
188 	enum WORLD_HEIGHT = worldHeight;
189 	alias WH = WORLD_HEIGHT;
190 	/// The window
191 	Window window;
192 	Tile[worldHeight][worldWidth] world; /// The world
193 	bool[charSize][charSize][256] chars; /// Bitmap of each character
194 	SysTime start; /// When the program was started
195 	float offset = 0; /// Offset to start at
196 	float endTime = float.infinity; /// Time to end at
197 	ulong frameCount; /// Current frame count.
198 	Entity[string] entities; /// Entities, indexed by ID
199 	/// Music being played
200 	Mix_Music* music = null;
201 
202 	/// The window class
203 	class Window {
204 		SDL_Renderer* rend;
205 		SDL_Window* win;
206 
207 		~this() {
208 			SDL_DestroyRenderer(rend);
209 			SDL_DestroyWindow(win);
210 		}
211 	}
212 	
213 	/// An image, backed by an SDL surface
214 	class Image {
215 		SDL_Texture* texture; /// A texture backing this image
216 		SDL_Surface* surface; /// The surface backing this image
217 
218 		/// Creates from an SDL surface. Note that you should only handle the texture thru this class after creating it (since the texture is deleted when the GC frees the instance)
219 		this(SDL_Surface *surf) {
220 			surface = surf;
221 			texture = SDL_CreateTextureFromSurface(window.rend, surf);
222 		}
223 
224 		/// Loads an image from a file
225 		static Image load(string file) {
226 			SDL_Surface* loaded = IMG_Load(file.toStringz);
227 			if(loaded == null)
228 				throw new SDLException("Could not load image file '"~file~"': "~SDL_GetError().to!string);
229 			scope(exit)
230 				SDL_FreeSurface(loaded);
231 			SDL_Surface* converted = SDL_ConvertSurfaceFormat(loaded, SDL_PIXELFORMAT_RGBA8888, 0);
232 			return new Image(converted);
233 		}
234 	
235 		/// Gets width
236 		@property int width() {
237 			return surface.w;
238 		}
239 		/// Gets height
240 		@property int height() {
241 			return surface.h;
242 		}
243 		/// Gets the pixels of this image
244 		@property Color[] pixels() {
245 			import std.algorithm, std.range, std.array;
246 			return (cast(ubyte*)surface.pixels)[0..surface.w*surface.h*4].chunks(4).map!(c => Color(c[1]/255f, c[2]/255f, c[3]/255f, c[0]/255f)).array;
247 		}
248 
249 		/// Frees the texture
250 		~this() {
251 			SDL_FreeSurface(surface);
252 			SDL_DestroyTexture(texture);
253 		}
254 	}
255 	/// Represents single thing that can happen on the screen
256 	abstract class Event {
257 		float start; /// When the event should appear
258 		float end; /// When the event should disappear (set to Infinity if the event should always be active)
259 		bool triggered; /// Whether this event has been triggered or not yet
260 		struct TileChange {
261 			Point pos;
262 			Tile prev;
263 		}
264 		TileChange[] changedTiles; /// Tiles changed by this event
265 		this(float start, float end) {
266 			this.start = start;
267 			this.end = end;
268 		}
269 
270 		/// Called when the event triggers
271 		void enable() {}
272 
273 		/// Called on the last frame of the event
274 		void disable() {}
275 
276 		/** Called every frame when the event is active. 
277 		
278 		`rel` is a value between 0 and 1, 0 being the first frame the event is visible, 1 being the last
279 
280 		`abs` is the amount of seconds since enable() has been called.
281 
282 		*/
283 		void time(float rel, float abs) {}
284 
285 		/// Changes the tile at (x, y) to the given tile
286 		void changeTile(Point p, Tile t) {
287 			Tile wt = world[p.x][p.y];
288 			if(t == wt)
289 				return; // no change needs to be done
290 			changedTiles ~= TileChange(p, wt);
291 			world[p.x][p.y] = t;
292 		}
293 
294 		/// Undoes all changed tiles
295 		void undoChanges() {
296 			foreach(p; changedTiles)
297 				world[p.pos.x][p.pos.y] = Tile([0, 0, 0], [0, 0, 0], ' ');
298 			// foreach(p; changedTiles)
299 			//	 world[p.pos.x][p.pos.y] = p.prev;
300 		}
301 	}
302 
303 	Event[] events; /// List of events queued
304 
305 	/// Queues an event
306 	void queue(T...)(T evts) {
307 		static foreach(e; evts) {
308 			static assert(is(typeof(e) : Event), "Only events can be queued!");
309 			if(offset >= e.end) {
310 				e.disable();
311 				continue;
312 			}
313 			if(offset >= e.start) {
314 				e.enable();
315 				e.triggered = true;
316 			}
317 			events ~= e;
318 		}
319 	}
320 
321 	/// Plays OGG audio
322 	void audio(string path) {
323 		if(doRender)
324 			return; // don't play audio if we're rendering
325 		if(music != null)
326 			return; // audio already playing
327 		music = Mix_LoadMUS(path.toStringz);
328 		if(music == null)
329 			throw new SDLException("Could not load audio: "~SDL_GetError().to!string);
330 		if(Mix_PlayMusic(music, 1) < 0)
331 			throw new SDLException("Could not play music: "~SDL_GetError().to!string);
332 		Mix_SetMusicPosition(offset);
333 	}
334 
335 	/// Puts text onto the screen
336 	void puts(bool hasEvent)(Event e, int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
337 		int sx = x;
338 		foreach(char c; text) {
339 			switch(c) {
340 				case '\n':
341 					y++;
342 					x = sx;
343 					break;
344 				case '\r':
345 					break; // grrr windows
346 				default:
347 					if(x < 0 || y < 0 || x >= worldWidth || y >= worldHeight)
348 						continue;
349 					static if(hasEvent) {
350 						if(replace || world[x][y].ch == ' ')
351 							e.changeTile(Point(x, y), Tile(bg, fg, c));
352 					} else {
353 						if(replace || world[x][y].ch == ' ')
354 							world[x][y] = Tile(bg, fg, c);
355 					}
356 					x++;
357 					break;
358 			}
359 		}
360 	}
361 
362 	/// Puts text with an event
363 	void puts(Event e, int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
364 		puts!true(e, x, y, bg, fg, text, replace);
365 	}
366 
367 	/// Puts text without an event
368 	void puts(int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
369 		puts!false(null, x, y, bg, fg, text, replace);
370 	}
371 
372 	/// Simple event to place text onto the screen at a given time
373 	class TextEvent : Event {
374 		string text;
375 		float[3] fg = [1, 1, 1];
376 		float[3] bg = [0, 0, 0];
377 		Point pos;
378 		bool replace;
379 		this(float start, float end, Point pos, float[3] fg, float[3] bg, string text, bool replace = true) {
380 			super(start, end);
381 			this.text = text;
382 			this.fg = fg;
383 			this.bg = bg;
384 			this.pos = pos;
385 			this.replace = replace;
386 		}
387 
388 		this(float start, float end, Point pos, float[3] fg, string text, bool replace = true) {
389 			super(start, end);
390 			this.text = text;
391 			this.fg = fg;
392 			this.pos = pos;
393 			this.replace = replace;
394 		}
395 
396 		this(float start, float end, Point pos, string text, bool replace = true) {
397 			super(start, end);
398 			this.pos = pos;
399 			this.text = text;
400 			this.replace = replace;
401 		}
402 
403 		override void enable() {
404 			puts(this, pos.x, pos.y, bg, fg, text, replace);
405 		}
406 
407 		override void disable() {
408 			undoChanges();
409 		}
410 	}
411 
412 	/// Types text onto the screen. Doesn't play well with easings that go backwards (easeBack, easeBounce)
413 	class TypeTextEvent : TextEvent {
414 		Easing ease;					/// Easing to use when typing the text. Default: `easeLinear`
415 		float typingTime = 1; /// Amount of time (seconds) to type the text
416 		this(float start, float end, Point pos, float[3] fg, float[3] bg, string text, Easing e = easing!"easeLinear", float typingTime = 0.5) {
417 			super(start, end, pos, fg, bg, text);
418 			ease = e;
419 			this.typingTime = typingTime;
420 		}
421 
422 		this(float start, float end, Point pos, string text, Easing e = easing!"easeLinear", float typingTime = 0.5) {
423 			super(start, end, pos, text);
424 			ease = e;
425 			this.typingTime = typingTime;
426 		}
427 
428 		override void enable() {}
429 		override void time(float rel, float abs) {
430 			float dif = abs-start;
431 			if(dif > typingTime)
432 				return;
433 			float t = ease(dif/typingTime);
434 			import std.math : ceil;
435 			int idx = cast(int)ceil(t*text.length);
436 			puts(this, pos.x, pos.y, bg, fg, text[0..idx], replace);
437 		}
438 	}
439 
440 	/// Flashing text.
441 	class FlashingTextEvent : TextEvent {
442 		float flashPeriod = 0.5; /// Period of flashing (in seconds)
443 		float[3] fg2; /// Second foreground color
444 		float[3] bg2; /// Second background color
445 		this(float start, float end, Point pos, float[3] fg, float[3] bg, float[3] fg2, float[3] bg2, string text, float flashPeriod = 0.5) {
446 			super(start, end, pos, fg, bg, text);
447 			this.fg2 = fg2;
448 			this.bg2 = bg2;
449 			this.flashPeriod = flashPeriod;
450 		}
451 
452 		override void time(float rel, float abs) {
453 			float dif = abs-start;
454 			if(dif%(flashPeriod*2) < flashPeriod)
455 				puts(pos.x, pos.y, bg, fg, text, replace);
456 			else
457 				puts(pos.x, pos.y, bg2, fg2, text, replace);
458 		}
459 	}
460 
461 	/// Creates a box using the +, -, and | characters
462 	class BoxEvent : Event {
463 		Point tl; /// Top left
464 		Point br; /// Bottom right
465 		float[3] fg = [1, 1, 1]; /// Foreground color
466 		float[3] bg = [0, 0, 0]; /// Background color
467 		this(float start, float end, Point topleft, Point bottomright, float[3] fg, float[3] bg) {
468 			super(start, end);
469 			this.tl = topleft;
470 			this.br = bottomright;
471 			this.fg = fg;
472 			this.bg = bg;
473 		}
474 
475 		this(float start, float end, Point topleft, Point bottomright, float[3] fg) {
476 			super(start, end);
477 			this.tl = topleft;
478 			this.br = bottomright;
479 			this.fg = fg;
480 			this.bg = bg;
481 		}
482 
483 		override void enable() {
484 			string rep(string s, int n) {
485 				import std.array : appender;
486 				auto ap = appender!string;
487 				for(int i = 0; i < n; i++)
488 					ap ~= s;
489 				return ap[];
490 			}
491 			import std.stdio;
492 			puts(this, tl.x+1, tl.y, bg, fg, rep("-", br.x-tl.x-1), true);
493 			puts(this, tl.x+1, br.y, bg, fg, rep("-", br.x-tl.x-1), true);
494 			puts(this, tl.x, tl.y+1, bg, fg, rep("|\n", br.y-tl.y-1), true);
495 			puts(this, br.x, tl.y+1, bg, fg, rep("|\n", br.y-tl.y-1), true);
496 			puts(this, tl.x, tl.y, bg, fg, "+", true);
497 			puts(this, tl.x, br.y, bg, fg, "+", true);
498 			puts(this, br.x, tl.y, bg, fg, "+", true);
499 			puts(this, br.x, br.y, bg, fg, "+", true);
500 		}
501 
502 		override void disable() {
503 			undoChanges();
504 		}
505 	}
506 
507 	float mapBetween(float x, float min0, float max0, float min1, float max1) {
508 		return (x-min0) / (max0-min0) * (max1-min1) + min1;
509 	}
510 
511 	/// Translates the screen from an origin to a destination over an amount of time
512 	class TranslationEvent : Event {
513 		Easing ease;
514 		Vector origin;
515 		Vector dest;
516 
517 		static Vector prevDest; /// The last constructed TranslationEvent's destination
518 
519 		this(float start, float end, Vector origin, Vector dest, Easing e = easing!"easeLinear") {
520 			super(start, end);
521 			ease = e;
522 			this.origin = origin;
523 			this.dest = dest;
524 			prevDest = dest;
525 		}
526 
527 		/// Origin is assumed to be `prevDest`
528 		this(float start, float end, Vector dest, Easing e = easing!"easeLinear") {
529 			super(start, end);
530 			ease = e;
531 			this.origin = prevDest;
532 			this.dest = dest;
533 			prevDest = dest;
534 		}
535 
536 		override void enable() {
537 			translation = origin;
538 		}
539 
540 		override void disable() {
541 			translation = dest;
542 		}
543 
544 		override void time(float rel, float abs) {
545 			float eased = ease(rel);
546 			translation.x = mapBetween(eased, 0, 1, origin.x, dest.x);
547 			translation.y = mapBetween(eased, 0, 1, origin.y, dest.y);
548 		}
549 	}
550 
551 	/// Changes zoom level by one value to another over an amount of time
552 	class ZoomEvent : Event {
553 		Easing ease;
554 		float first;
555 		float second;
556 
557 		static float prevSecond; /// Second of the last constructed ZoomEvent
558 
559 		/// 
560 		this(float start, float end, float first, float second, Easing e = easing!"easeLinear") {
561 			super(start, end);
562 			this.first = first;
563 			this.second = second;
564 			prevSecond = second;
565 			ease = e;
566 		}
567 		
568 		/// First is assumed to be prevSecond
569 		this(float start, float end, float second, Easing e = easing!"easeLinear") {
570 			super(start, end);
571 			this.first = prevSecond;
572 			this.second = second;
573 			prevSecond = second;
574 			ease = e;
575 		}
576 
577 		override void enable() {
578 			zoom = first;
579 		}
580 
581 		override void disable() {
582 			zoom = second;
583 		}
584 
585 		override void time(float rel, float abs) {
586 			float eased = ease(rel);
587 			zoom = mapBetween(eased, 0, 1, first, second);
588 		}
589 	}
590 	
591 	/// Anything that isn't text
592 	abstract class Entity {
593 		string id; /// Unique ID of this entity
594 		Vector pos, size; /// Position and size of the entity
595 		bool visible = false; /// Whether this entity is visible right now or not
596 
597 		/// Generates an ID (they look like entity-0, entity-1, entity-2, entity-3, etc)
598 		static string generateID() {
599 			static uint last = 0;
600 			return "entity-"~((last++).to!string);
601 		}
602 		
603 		///
604 		this(Vector pos, Vector size, string id = generateID()) {
605 			this.pos = pos;
606 			this.size = size;
607 			this.id = id;
608 			if(id in entities)
609 				throw new TexitException("There already exists an entity with ID '"~id~"'.");
610 			entities[id] = this;
611 		}
612 
613 		/// Shows the entity
614 		void show() {
615 			visible = true;
616 		}
617 		
618 		/// Hides the entity
619 		void hide() {
620 			visible = false;
621 		}
622 		
623 		/// Call this to render the entity
624 		final void render() {
625 			if(!visible)
626 				return;
627 			const float sc = (scale*2)/zoom; // SDL2's pixel I think is twice as small as openGL's, so that's why the 2 is here
628 			const float css = charSize*sc;
629 			const float tx = css*(zoom*width/4-translation.x), ty = css*sc*(zoom*height/4-translation.y);
630 			Vector spos = Vector(pos.x*css+tx, pos.y*css+ty);
631 			Vector ssize = Vector(size.x*css, size.y*css);
632 			render(spos, ssize);
633 		}
634 		/// Rendering code (this is what should be overriden)
635 		void render(Vector spos, Vector ssize) {}
636 	}
637 
638 	/// An image entity
639 	class ImageEntity : Entity {
640 		Image img; /// The image
641 		
642 		private final Vector getSize() {
643 			return Vector(img.width/(charSize*scale*2), img.height/(charSize*scale*2));
644 		}
645 
646 		this(Vector pos, Image img) {
647 			this.img = img;
648 			super(pos, getSize());
649 		}
650 
651 		this(Vector pos, string id, Image img) {
652 			this.img = img;
653 			super(pos, getSize(), id);
654 		}
655 		
656 		this(Vector pos, string filename) {
657 			this.img = Image.load(filename);
658 			super(pos, getSize());
659 		}
660 
661 		this(Vector pos, string id, string filename) {
662 			this.img = Image.load(filename);
663 			super(pos, getSize(), id);
664 		}
665 		
666 		this(Vector pos, Vector size, Image img) {
667 			this.img = img;
668 			super(pos, size);
669 		}
670 
671 		this(Vector pos, Vector size, string id, Image img) {
672 			this.img = img;
673 			super(pos, size, id);
674 		}
675 		
676 		this(Vector pos, Vector size, string filename) {
677 			this.img = Image.load(filename);
678 			super(pos, size);
679 		}
680 
681 		this(Vector pos, Vector size, string id, string filename) {
682 			this.img = Image.load(filename);
683 			super(pos, size, id);
684 		}
685 
686 		override void render(Vector spos, Vector ssize) {
687 			SDL_FRect dest = SDL_FRect(spos.x, spos.y, ssize.x, ssize.y);
688 			SDL_RenderCopyF(window.rend, img.texture, null, &dest);
689 		}
690 	}
691 	/// Event that creates an entity
692 	class EntityEvent : Event {
693 		Entity entity;
694 
695 		this(float start, float end, Entity e) {
696 			super(start, end);
697 			entity = e;
698 		}
699 
700 		override void enable() {
701 			entity.show();
702 		}
703 
704 		override void disable() {
705 			entity.hide();
706 		}
707 	}
708 	/// Event that changes an entity's position
709 	class EntityTranslationEvent : Event {
710 		Easing ease;
711 		string id;
712 		Vector origin, dest;
713 
714 		Entity entity() {
715 			return entities[id];
716 		}
717 
718 		this(float start, float end, string id, Vector origin, Vector dest, Easing e = easing!"easeLinear") {
719 			super(start, end);
720 			this.id = id;
721 			this.origin = origin;
722 			this.dest = dest;
723 			this.ease = e;
724 		}
725 
726 		override void enable() {
727 			entity.pos = origin;
728 		}
729 
730 		override void disable() {
731 			entity.pos = dest;
732 		}
733 
734 		override void time(float rel, float abs) {
735 			float eased = ease(rel);
736 			entity.pos.x = mapBetween(eased, 0, 1, origin.x, dest.x);
737 			entity.pos.y = mapBetween(eased, 0, 1, origin.y, dest.y);
738 		}
739 	}
740 	
741 	/// Event that changes an entity's size
742 	class EntityResizeEvent : Event {
743 		Easing ease;
744 		string id;
745 		Vector origin, dest;
746 
747 		Entity entity() {
748 			return entities[id];
749 		}
750 
751 		this(float start, float end, string id, Vector origin, Vector dest, Easing e = easing!"easeLinear") {
752 			super(start, end);
753 			this.id = id;
754 			this.origin = origin;
755 			this.dest = dest;
756 			this.ease = e;
757 		}
758 
759 		override void enable() {
760 			entity.size = origin;
761 		}
762 
763 		override void disable() {
764 			entity.size = dest;
765 		}
766 
767 		override void time(float rel, float abs) {
768 			float eased = ease(rel);
769 			entity.size.x = mapBetween(eased, 0, 1, origin.x, dest.x);
770 			entity.size.y = mapBetween(eased, 0, 1, origin.y, dest.y);
771 		}
772 	}
773 
774 	bool doRender; /// Whether to render to an image sequence
775 		
776 	/// Returns a charmap given a directory and a char size
777 	bool[charSize][charSize][256] loadCharmap(string path) {
778 		Image charmap = Image.load(path);
779 		int w = charmap.width, h = charmap.height;
780 		if(w != charSize*16 && h != charSize*16)
781 			throw new Exception("Charmap is incorrectly sized! Should be "~(16*charSize).to!string~"×"~(16*charSize).to!string~", but got "~w.to!string~"×"~h.to!string~".");
782 		Color[] pixels = charmap.pixels;
783 		bool[charSize][charSize][256] chars;
784 		// probably could be more efficient but eh
785 		for(int i = 0; i < 16; i++) {
786 			for(int j = 0; j < 16; j++) {
787 				for(int k = 0; k < charSize; k++) {
788 					for(int l = 0; l < charSize; l++) {
789 						import std.stdio;
790 						chars[j*16+i][k][l] = pixels[(j*charSize*w+i*charSize+l*w+k)].r != 0;
791 					}
792 				}
793 			}
794 		}
795 		return chars;
796 	}
797 
798 	void main(string[] args) {
799 		{
800 			auto opt = getopt(args,
801 				"i|imagesequence", "Render to an image sequence, outputted to the directory ./images.", &doRender
802 			);
803 			if(opt.helpWanted) {
804 				defaultGetoptPrinter("Options:", opt.options);
805 				return;
806 			}
807 		}
808 		if(doRender) {
809 			if("images".exists)
810 				rmdirRecurse("images");
811 			mkdir("images");
812 		}
813 		// init SDL and stuff
814 		if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0)
815 			throw new SDLException("Could not load SDL: "~SDL_GetError().to!string);
816 		if(Mix_Init(MIX_INIT_OGG) < 0)
817 			throw new SDLException("Could not load SDL_mixer: "~SDL_GetError().to!string);
818 		if(Mix_OpenAudio(44100, AUDIO_S16SYS, 2, 512))
819 			throw new SDLException("Could not open audio: "~SDL_GetError().to!string);
820 		window = new Window;
821 		if(SDL_CreateWindowAndRenderer(cast(int)(width*charSize*scale), cast(int)(height*charSize*scale), SDL_WINDOW_SHOWN, &window.win, &window.rend) < 0)
822 			throw new SDLException("Could not create window or renderer: "~SDL_GetError().to!string);
823 		SDL_SetWindowTitle(window.win, title.toStringz);
824 		scope(exit)
825 			if(music != null)
826 				Mix_FreeMusic(music);
827 		// for convenience; the window is always going to be cleaned up after this function ends so it should be fine to do this
828 		SDL_Renderer* rend = window.rend;
829 		// init translation
830 		translation = Vector(width/4, height/4);
831 		// load charmap
832 		chars = loadCharmap(charmap);
833 		// run start
834 		static if(__traits(compiles, setup()))
835 			setup();
836 		// set time
837 		start = Clock.currTime;
838 		// surface to save frames to (null if not used)
839 		SDL_Surface* frameSurface;
840 		if(doRender)
841 			frameSurface = SDL_CreateRGBSurface(0, cast(int)(width*charSize*scale), cast(int)(height*charSize*scale), 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);
842 		scope(exit)
843 			if(doRender)
844 				SDL_FreeSurface(frameSurface);
845 		// main loop
846 		outer: while(true) {
847 			// handle sdl events
848 			SDL_Event e;
849 			while(SDL_PollEvent(&e)) {
850 				switch(e.type) {
851 					case SDL_QUIT:
852 						break outer;
853 					default:
854 						break;
855 				}
856 			}
857 			// update other things
858 			auto dif = Clock.currTime-start;
859 			float time;
860 			if(!doRender)
861 				time = ((dif.total!"msecs")/1000f)+offset;
862 			else
863 				time = (1/30f)*frameCount;
864 			if(time > endTime)
865 				break outer;
866 			foreach_reverse(i, evt; events) {
867 				if(time >= evt.start && time <= evt.end) {
868 					if(!evt.triggered) {
869 						evt.enable();
870 						evt.triggered = true;
871 					}
872 					float rel = (time-evt.start)/(evt.end-evt.start);
873 					evt.time(rel, time);
874 				}
875 				else if(time > evt.end) {
876 					evt.disable();
877 					import std.algorithm : remove;
878 					events = events.remove(i);
879 				}
880 			}
881 			static if(__traits(compiles, loop()))
882 				loop();
883 			// render world
884 			const float sc = (scale*2)/zoom; // SDL2's pixel I think is twice as small as openGL's, so that's why the 2 is here
885 			const float css = charSize*sc;
886 			SDL_SetRenderDrawColor(rend, 0, 0, 0, 0);
887 			SDL_RenderClear(rend);
888 			// render entities first
889 			foreach(_, ent; entities)
890 				ent.render();
891 			const float tx = charSize*sc*(zoom*width/4-translation.x), ty = charSize*sc*(zoom*height/4-translation.y);
892 			for(int i = 0; i < worldWidth; i++) {
893 				for(int j = 0; j < worldHeight; j++) {
894 					auto tile = world[i][j];
895 					float x = i*css;
896 					float y = j*css;
897 					auto r = SDL_FRect(x+tx, y+ty, css, css);
898 					SDL_SetRenderDrawColor(rend, cast(ubyte)(tile.bg[0]*255), cast(ubyte)(tile.bg[1]*255), cast(ubyte)(tile.bg[2]*255), 255);
899 					// only render background color if it's not black
900 					if(tile.bg[0] != 0 || tile.bg[1] != 0 || tile.bg[2] != 0)
901 						SDL_RenderFillRectF(rend, &r);
902 					if(tile.ch == ' ')
903 						continue;
904 					auto ch = chars[tile.ch];
905 					SDL_SetRenderDrawColor(rend, cast(ubyte)(tile.fg[0]*255), cast(ubyte)(tile.fg[1]*255), cast(ubyte)(tile.fg[2]*255), 255);
906 					for(int k = 0; k < charSize; k++) {
907 						for(int l = 0; l < charSize; l++) {
908 							if(!ch[k][l])
909 								continue;
910 							r = SDL_FRect(x+k*sc+tx, y+l*sc+ty, sc, sc);
911 							SDL_RenderFillRectF(rend, &r);
912 						}
913 					}
914 				}
915 			}
916 			// create image if necessary
917 			if(doRender) {
918 				import std.format;
919 				SDL_RenderReadPixels(window.rend, null, SDL_PIXELFORMAT_ARGB8888, frameSurface.pixels, frameSurface.pitch);
920 				IMG_SavePNG(frameSurface, "./images/%08d.png".format(frameCount).toStringz);
921 			}
922 			frameCount++;
923 			SDL_RenderPresent(rend);
924 		}
925 		destroy(window);
926 		SDL_Quit();
927 	}
928 }