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 }