summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkdx <kikoodx@paranoici.org>2023-05-03 01:17:25 +0200
committerkdx <kikoodx@paranoici.org>2023-05-03 01:17:25 +0200
commitca2f55ac494245422e27fe272dddc5092ce0afc6 (patch)
tree6c7cd09707ff3b66c8fcb6d545ff6c8a8b87838a
downloadstudy-sttky-ca2f55ac494245422e27fe272dddc5092ce0afc6.tar.gz
base
-rw-r--r--.gitignore2
-rw-r--r--Tupfile16
-rw-r--r--Tupfile.ini0
-rwxr-xr-xbuildwin.sh13
-rw-r--r--compile_flags.txt4
-rwxr-xr-xemcc.sh7
-rw-r--r--map/game.tmj67
-rw-r--r--map/tset.tsj23
-rw-r--r--res/tset.pngbin0 -> 98 bytes
-rwxr-xr-xrun.sh2
-rw-r--r--src/TZR.c992
-rw-r--r--src/TZR.h417
-rw-r--r--src/camera.c48
-rw-r--r--src/camera.h7
-rw-r--r--src/cfg.h7
-rw-r--r--src/clamp.h19
-rw-r--r--src/entity.c174
-rw-r--r--src/entity.h69
-rw-r--r--src/entityimpl.h72
-rw-r--r--src/entitytag.c4
-rw-r--r--src/entitytag.h11
-rw-r--r--src/game.c182
-rw-r--r--src/game.h36
-rw-r--r--src/main.c88
-rw-r--r--src/map.c130
-rw-r--r--src/map.h21
-rw-r--r--src/player.c29
-rw-r--r--src/px.c260
-rw-r--r--src/px.h132
-rw-r--r--src/tiled2c.h61
-rw-r--r--src/tileset.c95
-rw-r--r--src/tileset.h12
32 files changed, 3000 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c03b489
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.tup
+/build
diff --git a/Tupfile b/Tupfile
new file mode 100644
index 0000000..5b0cbd1
--- /dev/null
+++ b/Tupfile
@@ -0,0 +1,16 @@
+CC = gcc
+LD = $(CC) -fuse-ld=mold
+CFLAGS = -O3 -std=c99 -Wall -Wextra -Wno-override-init \
+ -DPX_WIDTH=400 -DPX_HEIGHT=224
+LDFLAGS = -lm -lSDL2 -lSDL2_image -lSDL2_mixer
+
+# codebase
+: foreach map/*.tmj |> tiled2c %f embed_%B_tmj >%o |> build/embed_%B_tmj.c
+: foreach map/*.tsj |> tiled2c %f embed_%B_tsj >%o |> build/embed_%B_tsj.c
+: foreach src/*.c |> $(CC) $(CFLAGS) -c -o %o %f |> build/%B.o
+: foreach build/*.c |> $(CC) $(CFLAGS) -Isrc -c -o %o %f |> build/%B.o
+: build/*.o |> $(LD) -o %o %f $(LDFLAGS) |> build/study-sttky
+
+# assets
+: foreach res/*.wav |> cp %f %o |> build/%f
+: foreach res/*.png |> convert %f %o |> build/res/%B.bmp
diff --git a/Tupfile.ini b/Tupfile.ini
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tupfile.ini
diff --git a/buildwin.sh b/buildwin.sh
new file mode 100755
index 0000000..f0ea51a
--- /dev/null
+++ b/buildwin.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+tup || exit 1
+cd build || exit 1
+rm -rf ld53_win
+mkdir -p ld53_win || exit 1
+x86_64-w64-mingw32-gcc \
+ -o ld53_win/ld53.exe -I../src -Dmain=SDL_main \
+ *.c ../src/*.c \
+ -lmingw32 -lSDL2main -lSDL2 -lSDL2_mixer -lSDL2_image -mwindows \
+ || exit 1
+cp ~/dll/SDL2*.dll ld53_win || exit 1
+cp -r res ld53_win || exit 1
+zip -9r ld53.zip ld53_win || exit 1
diff --git a/compile_flags.txt b/compile_flags.txt
new file mode 100644
index 0000000..f45cb29
--- /dev/null
+++ b/compile_flags.txt
@@ -0,0 +1,4 @@
+-Wall
+-Wextra
+-Wno-initializer-overrides
+-std=c99
diff --git a/emcc.sh b/emcc.sh
new file mode 100755
index 0000000..97146d3
--- /dev/null
+++ b/emcc.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+tup
+cp src/index.html build
+cd build
+emcc -sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_MIXER=2 -sALLOW_MEMORY_GROWTH \
+ -Wno-initializer-overrides -std=c2x -O3 \
+ *.c ../src/*.c -I../src -o index.js --preload-file res
diff --git a/map/game.tmj b/map/game.tmj
new file mode 100644
index 0000000..f4c7dde
--- /dev/null
+++ b/map/game.tmj
@@ -0,0 +1,67 @@
+{ "compressionlevel":-1,
+ "height":14,
+ "infinite":false,
+ "layers":[
+ {
+ "data":[2, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1,
+ 2, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 0,
+ 2, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
+ 2, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 2, 2, 0, 2, 0, 0, 1, 1, 1, 0, 1, 1, 0,
+ 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 2, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0,
+ 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0,
+ 2, 0, 1, 0, 0, 0, 1, 0, 0, 2, 0, 1, 1, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0,
+ 2, 0, 1, 0, 0, 0, 1, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0,
+ 2, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0,
+ 2, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 2, 0, 0, 0,
+ 2, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 2, 2, 0,
+ 1, 1, 1, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 2,
+ 1, 2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
+ 2, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ "height":14,
+ "id":1,
+ "name":"Tile Layer 1",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":25,
+ "x":0,
+ "y":0
+ },
+ {
+ "draworder":"topdown",
+ "id":2,
+ "name":"Object Layer 1",
+ "objects":[
+ {
+ "height":16,
+ "id":1,
+ "name":"player",
+ "rotation":0,
+ "type":"",
+ "visible":true,
+ "width":16,
+ "x":176,
+ "y":144
+ }],
+ "opacity":1,
+ "type":"objectgroup",
+ "visible":true,
+ "x":0,
+ "y":0
+ }],
+ "nextlayerid":3,
+ "nextobjectid":2,
+ "orientation":"orthogonal",
+ "renderorder":"right-down",
+ "tiledversion":"1.10.1",
+ "tileheight":16,
+ "tilesets":[
+ {
+ "firstgid":1,
+ "source":"tset.tsj"
+ }],
+ "tilewidth":16,
+ "type":"map",
+ "version":"1.10",
+ "width":25
+} \ No newline at end of file
diff --git a/map/tset.tsj b/map/tset.tsj
new file mode 100644
index 0000000..54a7ecf
--- /dev/null
+++ b/map/tset.tsj
@@ -0,0 +1,23 @@
+{ "columns":2,
+ "image":"..\/res\/tset.png",
+ "imageheight":16,
+ "imagewidth":32,
+ "margin":0,
+ "name":"tset",
+ "spacing":0,
+ "tilecount":2,
+ "tiledversion":"1.10.1",
+ "tileheight":16,
+ "tiles":[
+ {
+ "id":0,
+ "type":"solid"
+ },
+ {
+ "id":1,
+ "type":"spike"
+ }],
+ "tilewidth":16,
+ "type":"tileset",
+ "version":"1.10"
+} \ No newline at end of file
diff --git a/res/tset.png b/res/tset.png
new file mode 100644
index 0000000..3cc3164
--- /dev/null
+++ b/res/tset.png
Binary files differ
diff --git a/run.sh b/run.sh
new file mode 100755
index 0000000..b5d243f
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+tup && build/study-sttky
diff --git a/src/TZR.c b/src/TZR.c
new file mode 100644
index 0000000..386e765
--- /dev/null
+++ b/src/TZR.c
@@ -0,0 +1,992 @@
+/* Licensing information can be found at the end of the file. */
+#include <math.h>
+#include <SDL2/SDL_error.h>
+#include <SDL2/SDL_events.h>
+#include <SDL2/SDL.h>
+#include <SDL2/SDL_image.h>
+#include <SDL2/SDL_log.h>
+#include <SDL2/SDL_mixer.h>
+#include <SDL2/SDL_mouse.h>
+#include <SDL2/SDL_render.h>
+#include <SDL2/SDL_rwops.h>
+#include <SDL2/SDL_scancode.h>
+#include <SDL2/SDL_timer.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include "TZR.h"
+#include <unistd.h>
+
+/* sources/drain.h */
+
+static void *drain_error(void)
+{
+ perror("TZR_LoadResourceTyped:drain");
+ return NULL;
+}
+
+static char *drain(FILE *fp, size_t *size)
+{
+ if (fseek(fp, 0, SEEK_END) < 0)
+ return drain_error();
+ *size = ftell(fp);
+ if (fseek(fp, 0, SEEK_SET) < 0)
+ return drain_error();
+ char *data = malloc(*size);
+ if (data == NULL)
+ return drain_error();
+ if (fread(data, 1, *size, fp) != *size)
+ return free(data), drain_error();
+ return data;
+}
+
+/* sources/sdl_error.h */
+
+static int sdl_error(int rv)
+{
+ SDL_Log("%s\n", SDL_GetError());
+ return rv;
+}
+/* sources/sdl_rect_to_rect.h */
+
+static void TZR_RectFromSDLRect(TZR_Rect *dest, const SDL_Rect *src)
+{
+ dest->from.x = src->x;
+ dest->from.y = src->y;
+ dest->to.x = src->x + src->w - 1;
+ dest->to.y = src->y + src->h - 1;
+}
+/* sources/globals.c */
+
+TZR_Config ___tzr_config = {0};
+TZR_Color ___tzr_color = {0.0f, 0.0f, 0.0f, 0.0f, 1.0f};
+TZR_Resource *___tzr_resources = NULL;
+size_t ___tzr_resources_capacity = 0;
+size_t ___tzr_resources_size = 0;
+SDL_Window *___tzr_window = NULL;
+SDL_Renderer *___tzr_renderer = NULL;
+SDL_Texture *___tzr_target = NULL;
+SDL_Texture *___tzr_target_light = NULL;
+unsigned long ___tzr_tick = 0;
+unsigned long ___tzr_next_time = 0;
+unsigned long ___tzr_min_dt = 0;
+int ___tzr_should_quit = 0;
+int ___tzr_mouse_x = 0;
+int ___tzr_mouse_y = 0;
+TZR_KeyState ___tzr_keystates[SDL_NUM_SCANCODES] = {0};
+TZR_KeyState ___tzr_mousestates[256] = {0}; //doc says than mouse button is u8
+float ___tzr_scale = 1.0;
+int ___tzr_off_x = 1.0;
+int ___tzr_off_y = 1.0;
+Mix_Music *___tzr_music = NULL;
+/* sources/TZR_CycleEvents.c */
+
+void
+next_state(TZR_KeyState *keystate)
+{
+ switch (*keystate) {
+ case TZR_KEYSTATE_RELEASE:
+ case TZR_KEYSTATE_UP:
+ *keystate = TZR_KEYSTATE_UP;
+ break;
+ case TZR_KEYSTATE_PRESS:
+ case TZR_KEYSTATE_DOWN:
+ *keystate = TZR_KEYSTATE_DOWN;
+ break;
+ default:
+ break;
+ }
+}
+
+void
+TZR_CycleEvents(void)
+{
+ TZR_ResourcesWatch();
+ for (int i = 0; i < SDL_NUM_SCANCODES; i++)
+ next_state(&___tzr_keystates[i]);
+ for (int i = 0; i < 256; i++)
+ next_state(&___tzr_mousestates[i]);
+ TZR_Event e;
+ while (TZR_PollEvent(&e))
+ ;
+}
+/* sources/TZR_DestroyResource.c */
+
+void
+TZR_DestroyResource(TZR_Resource *res, int free_path)
+{
+ if (res->path != NULL && free_path)
+ free(res->path);
+ switch (res->type) {
+ case TZR_RES_RAW:
+ if (res->raw.data != NULL)
+ free(res->raw.data);
+ break;
+ case TZR_RES_IMAGE:
+ if (res->image.ptr != NULL)
+ SDL_DestroyTexture(res->image.ptr);
+ break;
+ case TZR_RES_SOUND:
+ if (res->sound.ptr != NULL)
+ Mix_FreeChunk(res->sound.ptr);
+ break;
+ default:
+ fprintf(stderr, "unknown resource type %u\n", res->type);
+ break;
+ }
+}
+
+/* sources/TZR_DirectResourceLoad.c */
+
+int
+TZR_DirectResourceLoad(TZR_Resource *res, const void *data, int size)
+{
+ switch (res->type) {
+ case TZR_RES_RAW:
+ res->raw.size = size;
+ res->raw.data = malloc(size);
+ if (res->raw.data == NULL) {
+ perror("TZR_DirectResourceLoad");
+ return -1;
+ }
+ memcpy(res->raw.data, data, size);
+ break;
+ case TZR_RES_IMAGE: {
+ SDL_RWops *const rw = SDL_RWFromConstMem(data, size);
+ if (rw == NULL)
+ return sdl_error(-1);
+ SDL_Surface *surf = IMG_Load_RW(rw, 1);
+ if (surf == NULL) {
+ SDL_RWops *const rw = SDL_RWFromConstMem(data, size);
+ if (rw == NULL)
+ return sdl_error(-1);
+ surf = SDL_LoadBMP_RW(rw, 1);
+ }
+ if (surf == NULL)
+ return sdl_error(-1);
+ SDL_Texture *const tex =
+ SDL_CreateTextureFromSurface(___tzr_renderer, surf);
+ SDL_FreeSurface(surf);
+ if (tex == NULL)
+ return sdl_error(-1);
+ if (SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND) < 0) {
+ SDL_DestroyTexture(tex);
+ return sdl_error(-1);
+ }
+ if (SDL_QueryTexture(tex, NULL, NULL,
+ &res->image.width, &res->image.height)) {
+ SDL_DestroyTexture(tex);
+ return sdl_error(-1);
+ }
+ res->image.ptr = tex;
+ } break;
+ case TZR_RES_SOUND: {
+ if (!___tzr_config.mixer) {
+ SDL_Log("audio mixer disabled, skip loading");
+ return -1;
+ }
+ SDL_RWops *const rw = SDL_RWFromConstMem(data, size);
+ if (rw == NULL)
+ return sdl_error(-1);
+ Mix_Chunk *const chunk = Mix_LoadWAV_RW(rw, 0);
+ SDL_RWclose(rw);
+ if (chunk == NULL)
+ return sdl_error(-1);
+ res->sound.ptr = chunk;
+ } break;
+ default:
+ fprintf(stderr, "invalid type\n");
+ return -1;
+ }
+ return 0;
+}
+/* sources/TZR_DrawBegin.c */
+
+int
+TZR_DrawBegin(void)
+{
+ if (SDL_SetRenderTarget(___tzr_renderer, ___tzr_target) < 0)
+ return sdl_error(-1);
+ if (SDL_SetRenderDrawBlendMode(___tzr_renderer, SDL_BLENDMODE_BLEND))
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_DrawClear.c */
+
+int
+TZR_DrawClear(void)
+{
+ if (SDL_RenderClear(___tzr_renderer) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_DrawEnd.c */
+
+int
+TZR_DrawEnd(void)
+{
+ if (SDL_SetRenderTarget(___tzr_renderer, NULL) < 0)
+ return sdl_error(-1);
+ if (SDL_SetRenderDrawColor(___tzr_renderer, 0, 0, 0, 255) < 0)
+ return sdl_error(-1);
+ if (SDL_RenderClear(___tzr_renderer) < 0)
+ return sdl_error(-1);
+
+ int win_w, win_h;
+ SDL_GetWindowSize(___tzr_window, &win_w, &win_h);
+ win_w = win_w ? win_w : 1;
+ win_h = win_h ? win_h : 1;
+
+ const float ratio_w = win_w / (float)___tzr_config.width;
+ const float ratio_h = win_h / (float)___tzr_config.height;
+ ___tzr_scale = (ratio_w < ratio_h) ? ratio_w : ratio_h;
+ if (___tzr_scale > 1.0f && ___tzr_config.pixel_perfect)
+ ___tzr_scale = (int)___tzr_scale;
+ ___tzr_off_x = (win_w - ___tzr_config.width * ___tzr_scale) / 2;
+ ___tzr_off_y = (win_h - ___tzr_config.height * ___tzr_scale) / 2;
+ const SDL_Rect dest = {
+ ___tzr_off_x,
+ ___tzr_off_y,
+ ___tzr_config.width * ___tzr_scale,
+ ___tzr_config.height * ___tzr_scale
+ };
+
+ if (SDL_RenderCopy(___tzr_renderer, ___tzr_target, NULL, &dest) < 0)
+ return sdl_error(-1);
+ if (___tzr_config.light_system) {
+ if (SDL_SetTextureBlendMode(___tzr_target_light,
+ SDL_BLENDMODE_MOD) < 0)
+ return sdl_error(-1);
+ if (SDL_RenderCopy(___tzr_renderer, ___tzr_target_light, NULL,
+ &dest) < 0)
+ return sdl_error(-1);
+ }
+
+ if (___tzr_config.target_fps > 0) {
+ ___tzr_next_time += ___tzr_min_dt;
+ const unsigned long cur_time = SDL_GetTicks64();
+ if (___tzr_next_time <= cur_time)
+ ___tzr_next_time = cur_time;
+ else
+ SDL_Delay(___tzr_next_time - cur_time);
+ }
+ SDL_RenderPresent(___tzr_renderer);
+
+ ___tzr_tick += 1;
+ return 0;
+}
+/* sources/TZR_DrawImage.c */
+
+int
+_TZR_DrawImage(const TZR_DrawImageArgs *args)
+{
+ if (args->id == 0) {
+ fprintf(stderr, "args->id is 0\n");
+ return -1;
+ }
+
+ if (TZR_GetResourceType(args->id) != TZR_RES_IMAGE) {
+ fprintf(stderr, "%u isn't an image\n", args->id);
+ return -1;
+ }
+ TZR_Resource *const res = TZR_GetResourcePointer(args->id);
+ const TZR_Image *const img = &res->image;
+
+ if (SDL_SetTextureColorMod(img->ptr, ___tzr_color.r * 255,
+ ___tzr_color.g * 255, ___tzr_color.b * 255))
+ return sdl_error(-1);
+ if (SDL_SetTextureAlphaMod(img->ptr, ___tzr_color.a * 255))
+ return sdl_error(-1);
+
+ float scale_x = args->sx;
+ float scale_y = args->sy;
+
+ int simg_width = (args->w == INT_MIN) ? img->width : args->w;
+ if (simg_width < 0) {
+ simg_width *= -1;
+ scale_x *= -1;
+ }
+ if (simg_width + args->ix > img->width)
+ simg_width = img->width - args->ix;
+
+ int simg_height = (args->h == INT_MIN) ? img->height : args->h;
+ if (simg_height < 0) {
+ simg_height *= -1;
+ scale_y *= -1;
+ }
+ if (simg_height + args->iy > img->height)
+ simg_height = img->height - args->iy;
+
+ int flip = 0;
+ flip |= SDL_FLIP_HORIZONTAL * ((scale_x < 0.0f) ^ args->flip_x);
+ flip |= SDL_FLIP_VERTICAL * ((scale_y < 0.0f) ^ args->flip_y);
+ const SDL_Rect src = {
+ args->ix,
+ args->iy,
+ simg_width,
+ simg_height
+ };
+
+ const int width = simg_width * fabs(scale_x);
+ const int height = simg_height * fabs(scale_y);
+ const int x = args->x - (scale_x < 0.0f ? width : 0);
+ const int y = args->y - (scale_y < 0.0f ? height : 0);
+
+ const SDL_Rect dest = {
+ x - (args->center ? (simg_width * scale_x / 2) : 0),
+ y - (args->center ? (simg_height * scale_y / 2) : 0),
+ width,
+ height
+ };
+ if (SDL_RenderCopyEx(___tzr_renderer, img->ptr, &src, &dest,
+ args->r * 360, NULL, flip) < 0)
+ return sdl_error(-1);
+ TZR_Rect area;
+ TZR_RectFromSDLRect(&area, &dest);
+ return 0;
+}
+/* sources/TZR_DrawLine.c */
+
+int
+TZR_DrawLine(int x0, int y0, int x1, int y1)
+{
+ if (SDL_RenderDrawLine(___tzr_renderer, x0, y0, x1, y1) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_DrawPoint.c */
+
+int
+TZR_DrawPoint(int x, int y)
+{
+ if (SDL_RenderDrawPoint(___tzr_renderer, x, y) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_DrawRectangle.c */
+
+int
+_TZR_DrawRectangle(const TZR_DrawRectangleArgs *args)
+{
+ const SDL_Rect rect = {
+ args->x - args->center * (args->w / 2),
+ args->y - args->center * (args->h / 2),
+ args->w,
+ args->h
+ };
+ if (args->fill) {
+ if (SDL_RenderFillRect(___tzr_renderer, &rect) < 0)
+ return sdl_error(-1);
+ } else {
+ if (SDL_RenderDrawRect(___tzr_renderer, &rect) < 0)
+ return sdl_error(-1);
+ }
+ return 0;
+}
+/* sources/TZR_DrawSetColor8.c */
+
+int
+TZR_DrawSetColor8(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
+{
+ ___tzr_color.r = (float)r / 255.0f;
+ ___tzr_color.g = (float)g / 255.0f;
+ ___tzr_color.b = (float)b / 255.0f;
+ ___tzr_color.a = (float)a / 255.0f;
+ if (SDL_SetRenderDrawColor(___tzr_renderer, r, g, b, a) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_DrawSetColor.c */
+
+int
+_TZR_DrawSetColor(const TZR_Color *color)
+{
+ ___tzr_color.r = (color->r < 0.0f) ? ___tzr_color.r : color->r;
+ ___tzr_color.g = (color->g < 0.0f) ? ___tzr_color.g : color->g;
+ ___tzr_color.b = (color->b < 0.0f) ? ___tzr_color.b : color->b;
+ ___tzr_color.a = (color->a < 0.0f) ? ___tzr_color.a : color->a;
+ const uint8_t r = ___tzr_color.r * 255;
+ const uint8_t g = ___tzr_color.g * 255;
+ const uint8_t b = ___tzr_color.b * 255;
+ const uint8_t a = ___tzr_color.a * 255;
+ if (SDL_SetRenderDrawColor(___tzr_renderer, r, g, b, a) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_GetImageHeight.c */
+
+int
+TZR_GetImageHeight(TZR_Uint id)
+{
+ return TZR_GetResourcePointer(id)->image.height;
+}
+/* sources/TZR_GetImageWidth.c */
+
+int
+TZR_GetImageWidth(TZR_Uint id)
+{
+ return TZR_GetResourcePointer(id)->image.width;
+}
+/* sources/TZR_GetKeyState.c */
+
+TZR_KeyState
+TZR_GetKeyState(int scancode)
+{
+ return ___tzr_keystates[scancode];
+}
+/* sources/TZR_GetMousePosition.c */
+
+void
+TZR_GetMousePosition(int *x, int *y)
+{
+ if (x != NULL)
+ *x = ___tzr_mouse_x;
+ if (y != NULL)
+ *y = ___tzr_mouse_y;
+ TZR_ScreenTransform(x, y);
+}
+/* sources/TZR_GetRawResource.c */
+
+TZR_Raw *
+TZR_GetRawResource(TZR_Uint id)
+{
+ return &TZR_GetResourcePointer(id)->raw;
+}
+/* sources/TZR_GetResourcePath.c */
+
+const char *
+TZR_GetResourcePath(TZR_Uint id)
+{
+ return TZR_GetResourcePointer(id)->path;
+}
+/* sources/TZR_GetResourcePointer.c */
+
+TZR_Resource *
+TZR_GetResourcePointer(TZR_Uint id)
+{
+ return &___tzr_resources[id - 1];
+}
+/* sources/TZR_GetResourceType.c */
+
+TZR_ResourceType
+TZR_GetResourceType(TZR_Uint id)
+{
+ return TZR_GetResourcePointer(id)->type;
+}
+/* sources/TZR_GetTick.c */
+
+unsigned long
+TZR_GetTick(void)
+{
+ return ___tzr_tick;
+}
+/* sources/TZR_Init.c */
+
+static int
+_sdl_error(void)
+{
+ sdl_error(-1);
+ TZR_Quit();
+ return -1;
+}
+
+int
+_TZR_Init(const TZR_Config *config)
+{
+ memcpy(&___tzr_config, config, sizeof(TZR_Config));
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0)
+ return _sdl_error();
+ if (IMG_Init(IMG_INIT_PNG * ___tzr_config.png_loading) < 0)
+ return _sdl_error();
+ if (___tzr_config.mixer == TZR_MIXER_FLAC &&
+ Mix_Init(MIX_INIT_FLAC) != MIX_INIT_FLAC)
+ {
+ SDL_Log("%s", Mix_GetError());
+ ___tzr_config.mixer = TZR_MIXER_OFF;
+ }
+
+ char *const basepath = SDL_GetBasePath();
+ if (basepath == NULL)
+ return _sdl_error();
+ const int chdir_rv = chdir(basepath);
+ SDL_free(basepath);
+ if (chdir_rv < 0)
+ return perror("TZR_Init"), -1;
+
+ ___tzr_window =
+ SDL_CreateWindow("TZR",
+ SDL_WINDOWPOS_UNDEFINED,
+ SDL_WINDOWPOS_UNDEFINED,
+ ___tzr_config.width * ___tzr_config.scale,
+ ___tzr_config.height * ___tzr_config.scale,
+ SDL_WINDOW_RESIZABLE);
+ if (___tzr_window == NULL)
+ return _sdl_error();
+
+#ifdef __EMSCRIPTEN__
+ ___tzr_renderer = SDL_CreateRenderer(___tzr_window, -1,
+ SDL_RENDERER_SOFTWARE);
+#else
+ ___tzr_renderer = SDL_CreateRenderer(___tzr_window, -1,
+ SDL_RENDERER_ACCELERATED);
+#endif
+ if (___tzr_renderer == NULL)
+ return _sdl_error();
+
+ ___tzr_target = SDL_CreateTexture(___tzr_renderer,
+ SDL_PIXELFORMAT_RGB888,
+ SDL_TEXTUREACCESS_TARGET,
+ ___tzr_config.width,
+ ___tzr_config.height);
+ if (___tzr_target == NULL)
+ return _sdl_error();
+
+ if (___tzr_config.light_system) {
+ ___tzr_target_light = SDL_CreateTexture(___tzr_renderer,
+ SDL_PIXELFORMAT_RGB888,
+ SDL_TEXTUREACCESS_TARGET,
+ ___tzr_config.width,
+ ___tzr_config.height);
+ if (___tzr_target_light == NULL)
+ return _sdl_error();
+ }
+
+ SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, 0);
+ ___tzr_tick = 0;
+
+ if (___tzr_config.target_fps > 0) {
+ ___tzr_min_dt = 1000 / ___tzr_config.target_fps;
+ ___tzr_next_time = SDL_GetTicks64();
+ }
+
+ if (!___tzr_config.show_cursor)
+ SDL_ShowCursor(SDL_FALSE);
+ ___tzr_mouse_x = ___tzr_config.width / 2;
+ ___tzr_mouse_y = ___tzr_config.height / 2;
+
+ /* Setup audio. */
+ if (___tzr_config.mixer) {
+ if (Mix_OpenAudio(48000, MIX_DEFAULT_FORMAT, 8, 1024) < 0) {
+ sdl_error(0);
+ Mix_Quit();
+ ___tzr_config.mixer = false;
+ }
+ }
+ return 0;
+}
+/* sources/TZR_IsKeyDown.c */
+
+bool
+TZR_IsKeyDown(int scancode)
+{
+ const TZR_KeyState state = TZR_GetKeyState(scancode);
+ return (state == TZR_KEYSTATE_DOWN || state == TZR_KEYSTATE_PRESS);
+}
+/* sources/TZR_IsKeyPressed.c */
+
+bool
+TZR_IsKeyPressed(int scancode)
+{
+ return (TZR_GetKeyState(scancode) == TZR_KEYSTATE_PRESS);
+}
+/* sources/TZR_IsKeyReleased.c */
+
+bool
+TZR_IsKeyReleased(int scancode)
+{
+ if (TZR_GetKeyState(scancode) == TZR_KEYSTATE_RELEASE)
+ return true;
+ return false;
+}
+/* sources/TZR_IsKeyUp.c */
+
+bool
+TZR_IsKeyUp(int scancode)
+{
+ const TZR_KeyState state = TZR_GetKeyState(scancode);
+ return (state == TZR_KEYSTATE_UP || state == TZR_KEYSTATE_RELEASE);
+}
+/* sources/TZR_LightBegin.c */
+
+int
+TZR_LightBegin(void)
+{
+ if (!___tzr_config.light_system) {
+ fprintf(stderr, "light system disabled\n");
+ return -1;
+ }
+ if (SDL_SetRenderDrawBlendMode(___tzr_renderer, SDL_BLENDMODE_ADD) < 0)
+ return sdl_error(-1);
+ if (SDL_SetRenderTarget(___tzr_renderer, ___tzr_target_light) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_LightEnd.c */
+
+int
+TZR_LightEnd(void)
+{
+ return ___tzr_config.light_system ? 0 : -1;
+}
+/* sources/TZR_LoadResource.c */
+
+static TZR_ResourceType
+deduce_type(const char *path)
+{
+ const char *const path_extension = strrchr(path, '.');
+ if (path_extension == NULL)
+ return TZR_RES_RAW;
+ if (strcasecmp(path_extension, ".bmp") == 0 ||
+ strcasecmp(path_extension, ".png") == 0 ||
+ strcasecmp(path_extension, ".qoi") == 0)
+ return TZR_RES_IMAGE;
+ if (strcasecmp(path_extension, ".wav") == 0)
+ return TZR_RES_SOUND;
+ return TZR_RES_RAW;
+}
+
+TZR_Uint
+TZR_LoadResource(const char *path)
+{
+ return TZR_LoadResourceTyped(deduce_type(path), path);
+}
+/* sources/TZR_LoadResourceFromMemory.c */
+
+static int
+reserve_ressources(size_t size)
+{
+ if (size <= ___tzr_resources_capacity)
+ return 0;
+ size_t target = 16;
+ while (target < size * sizeof(TZR_Resource))
+ target *= 2;
+ TZR_Resource *new_resources;
+ if (___tzr_resources == NULL)
+ new_resources = malloc(target);
+ else
+ new_resources = realloc(___tzr_resources, target);
+ if (new_resources == NULL) {
+ perror("TZR_LoadResourceFromMemory:reserve_ressources");
+ return 1;
+ }
+ ___tzr_resources = new_resources;
+ return 0;
+}
+
+TZR_Uint
+TZR_LoadResourceFromMemory(TZR_ResourceType type, const void *data, int size)
+{
+ if (reserve_ressources(___tzr_resources_size + 1)) {
+ fprintf(stderr, "failed to reserve for new ressource\n");
+ return 0;
+ }
+ TZR_Resource *const res = &___tzr_resources[___tzr_resources_size];
+ res->type = type;
+ res->path = NULL;
+ if (TZR_DirectResourceLoad(res, data, size))
+ return 0;
+ ___tzr_resources_size += 1;
+ return ___tzr_resources_size;
+}
+/* sources/TZR_LoadResourceTyped.c */
+
+TZR_Uint
+TZR_LoadResourceTyped(TZR_ResourceType type, const char *path)
+{
+ for (TZR_Uint i = 0; i < ___tzr_resources_size; i++)
+ if (___tzr_resources[i].type == type &&
+ strcmp(___tzr_resources[i].path, path) == 0)
+ return i + 1;
+ FILE *const fp = fopen(path, "rb");
+ if (fp == NULL) {
+ perror(path);
+ return 0;
+ }
+ size_t size;
+ char *const data = drain(fp, &size);
+ fclose(fp);
+ if (data == NULL)
+ return 0;
+ const TZR_Uint id = TZR_LoadResourceFromMemory(type, data, size);
+ free(data);
+ if (id == 0)
+ return 0;
+ TZR_Resource *const res = TZR_GetResourcePointer(id);
+
+ const size_t path_len = strlen(path);
+ struct stat st = {0};
+ (void)stat(path, &st);
+ res->path = malloc(path_len + 1);
+ res->mtime = 0;
+ if (res->path == NULL)
+ perror("TZR_LoadResourceTyped");
+ else
+ strcpy(res->path, path);
+ return id;
+}
+/* sources/TZR_MainLoop.c */
+
+#ifdef __EMSCRIPTEN__
+
+static int (*_main_loop)(void *udata);
+static void *_udata;
+static void
+_em_loop(void)
+{
+ TZR_CycleEvents();
+ _main_loop(_udata);
+}
+
+int
+TZR_MainLoop(int (*main_loop)(void *udata), void *udata)
+{
+ _main_loop = main_loop;
+ _udata = udata;
+ emscripten_set_main_loop(_em_loop, 0, 1);
+ return 0;
+}
+
+#else
+
+int
+TZR_MainLoop(int (*main_loop)(void *udata), void *udata)
+{
+ while (!TZR_ShouldQuit()) {
+ TZR_CycleEvents();
+ if (main_loop(udata))
+ return 1;
+ }
+ return 0;
+}
+
+#endif
+/* sources/TZR_PlayMusic.c */
+
+int
+TZR_PlayMusic(const char *path, int loop)
+{
+ if (!___tzr_config.mixer) {
+ fprintf(stderr, "mixer is disabled\n");
+ return -1;
+ }
+ TZR_StopMusic();
+ ___tzr_music = Mix_LoadMUS(path);
+ if (___tzr_music == NULL)
+ return sdl_error(-1);
+ if (Mix_PlayMusic(___tzr_music, loop) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_PlaySound.c */
+
+int
+_TZR_PlaySound(const TZR_PlaySoundArgs *args)
+{
+ if (args->id == 0) {
+ fprintf(stderr, "args->id is 0\n");
+ return -1;
+ }
+
+ if (TZR_GetResourceType(args->id) != TZR_RES_SOUND) {
+ fprintf(stderr, "%u isn't a sound\n", args->id);
+ return -1;
+ }
+
+ TZR_Sound *const sound = &TZR_GetResourcePointer(args->id)->sound;
+ Mix_VolumeChunk(sound->ptr, args->volume * MIX_MAX_VOLUME);
+ if (Mix_PlayChannel(-1, sound->ptr, args->loop) < 0)
+ return sdl_error(-1);
+ return 0;
+}
+/* sources/TZR_PollEvent.c */
+
+int
+TZR_PollEvent(TZR_Event *e)
+{
+ TZR_Event discard;
+ if (e == NULL)
+ e = &discard;
+
+ SDL_Event se;
+ while (SDL_PollEvent(&se)) switch (se.type) {
+ case SDL_QUIT:
+ e->type = TZR_EV_QUIT;
+ ___tzr_should_quit = 1;
+ return 1;
+ case SDL_KEYDOWN:
+ if (se.key.repeat)
+ break;
+ e->type = TZR_EV_KEYDOWN;
+ e->button = se.key.keysym.scancode;
+ ___tzr_keystates[e->button] = TZR_KEYSTATE_PRESS;
+ break;
+ case SDL_KEYUP:
+ e->type = TZR_EV_KEYUP;
+ e->button = se.key.keysym.scancode;
+ ___tzr_keystates[e->button] = TZR_KEYSTATE_RELEASE;
+ break;
+ case SDL_MOUSEBUTTONDOWN:
+ ___tzr_mouse_x = se.button.x;
+ ___tzr_mouse_y = se.button.y;
+ ___tzr_mousestates[se.button.button] = TZR_KEYSTATE_PRESS;
+ break;
+ case SDL_MOUSEBUTTONUP:
+ ___tzr_mouse_x = se.button.x;
+ ___tzr_mouse_y = se.button.y;
+ ___tzr_mousestates[se.button.button] = TZR_KEYSTATE_RELEASE;
+ break;
+ case SDL_MOUSEMOTION:
+ ___tzr_mouse_x = se.motion.x;
+ ___tzr_mouse_y = se.motion.y;
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+/* sources/TZR_Quit.c */
+
+void
+TZR_Quit(void)
+{
+ for (size_t i = 0; i < ___tzr_resources_size; i++)
+ TZR_DestroyResource(&___tzr_resources[i], 1);
+ if (___tzr_resources != NULL)
+ free(___tzr_resources);
+ ___tzr_resources = NULL;
+ ___tzr_resources_size = 0;
+ ___tzr_resources_capacity = 0;
+ if (___tzr_target_light != NULL) {
+ SDL_DestroyTexture(___tzr_target_light);
+ ___tzr_target_light = NULL;
+ }
+ if (___tzr_target != NULL) {
+ SDL_DestroyTexture(___tzr_target);
+ ___tzr_target = NULL;
+ }
+ if (___tzr_renderer != NULL) {
+ SDL_DestroyRenderer(___tzr_renderer);
+ ___tzr_renderer = NULL;
+ }
+ if (___tzr_window != NULL) {
+ SDL_DestroyWindow(___tzr_window);
+ ___tzr_window = NULL;
+ }
+ if (___tzr_config.mixer) {
+ Mix_CloseAudio();
+ Mix_Quit();
+ }
+ IMG_Quit();
+ SDL_Quit();
+}
+/* sources/TZR_ReloadResource.c */
+
+int
+TZR_ReloadResource(TZR_Uint id)
+{
+ TZR_Resource *const res = TZR_GetResourcePointer(id);
+ if (res->path == NULL) {
+ fprintf(stderr, "resource path of %u is NULL\n", id);
+ return -1;
+ }
+ FILE *const fp = fopen(res->path, "rb");
+ if (fp == NULL) {
+ perror(res->path);
+ return -1;
+ }
+ size_t size;
+ char *const data = drain(fp, &size);
+ fclose(fp);
+ if (data == NULL)
+ return -1;
+ TZR_Resource new_res;
+ memcpy(&new_res, res, sizeof(TZR_Resource));
+ const int load_rv = TZR_DirectResourceLoad(&new_res, data, size);
+ free(data);
+ if (load_rv)
+ return -1;
+ TZR_DestroyResource(res, 0);
+ memcpy(res, &new_res, sizeof(TZR_Resource));
+ return 0;
+}
+/* sources/TZR_ResourcesWatch.c */
+
+void
+TZR_ResourcesWatch(void)
+{
+ for (TZR_Uint i = 0; i < ___tzr_resources_size; i++) {
+ TZR_Resource *const res = TZR_GetResourcePointer(i + 1);
+ if (res->path == NULL)
+ continue;
+ struct stat st = {0};
+ (void)stat(res->path, &st);
+ if (st.st_mtime != res->mtime) {
+ if (res->mtime != 0)
+ TZR_ReloadResource(i + 1);
+ res->mtime = st.st_mtime;
+ }
+ }
+}
+/* sources/TZR_ScreenTransform.c */
+
+void
+TZR_ScreenTransform(int *x, int *y)
+{
+ if (___tzr_scale == 0.0)
+ return;
+ if (x != NULL) {
+ *x -= ___tzr_off_x;
+ *x /= ___tzr_scale;
+ }
+ if (y != NULL) {
+ *y -= ___tzr_off_y;
+ *y /= ___tzr_scale;
+ }
+}
+/* sources/TZR_ShouldQuit.c */
+
+int
+TZR_ShouldQuit(void)
+{
+ return ___tzr_should_quit;
+}
+/* sources/TZR_StopMusic.c */
+
+void
+TZR_StopMusic(void)
+{
+ if (!___tzr_config.mixer)
+ return;
+ if (Mix_PlayingMusic())
+ Mix_HaltMusic();
+ if (___tzr_music != NULL)
+ Mix_FreeMusic(___tzr_music);
+ ___tzr_music = NULL;
+}
+
+/*
+** Copyright (c) 2023 kdx
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+*/
+/* commit hash: a4112e4311eb226c4f43cb697aa7f6c34bd7b9a5 */
diff --git a/src/TZR.h b/src/TZR.h
new file mode 100644
index 0000000..9fcf126
--- /dev/null
+++ b/src/TZR.h
@@ -0,0 +1,417 @@
+/* Licensing information can be found at the end of the file. */
+#pragma once
+#include <limits.h>
+#include <SDL2/SDL_main.h>
+#include <SDL2/SDL_mixer.h>
+#include <SDL2/SDL_render.h>
+#include <SDL2/SDL_scancode.h>
+#include <stdbool.h>
+#include <stddef.h>
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
+
+/* headers/TZR_types.h */
+
+enum TZR_ResourceType {
+ TZR_RES_RAW,
+ TZR_RES_IMAGE,
+ TZR_RES_SOUND,
+ ___TZR_RES_COUNT
+};
+
+enum TZR_EventType {
+ TZR_EV_QUIT,
+ TZR_EV_KEYDOWN,
+ TZR_EV_KEYUP
+};
+
+enum TZR_KeyState {
+ TZR_KEYSTATE_UP,
+ TZR_KEYSTATE_DOWN,
+ TZR_KEYSTATE_RELEASE,
+ TZR_KEYSTATE_PRESS
+};
+
+enum TZR_MixerFlags {
+ TZR_MIXER_OFF,
+ TZR_MIXER_ON,
+ TZR_MIXER_FLAC
+};
+
+typedef unsigned int TZR_Uint;
+typedef struct TZR_Config TZR_Config;
+typedef struct TZR_Point TZR_Point;
+typedef struct TZR_Color TZR_Color;
+typedef struct TZR_Rect TZR_Rect;
+typedef enum TZR_ResourceType TZR_ResourceType;
+typedef struct TZR_Raw TZR_Raw;
+typedef struct TZR_Image TZR_Image;
+typedef struct TZR_Sound TZR_Sound;
+typedef struct TZR_Resource TZR_Resource;
+typedef enum TZR_EventType TZR_EventType;
+typedef struct TZR_Event TZR_Event;
+typedef struct TZR_Music TZR_Music;
+typedef struct TZR_DrawImageArgs TZR_DrawImageArgs;
+typedef struct TZR_DrawRectangleArgs TZR_DrawRectangleArgs;
+typedef struct TZR_PlaySoundArgs TZR_PlaySoundArgs;
+typedef enum TZR_KeyState TZR_KeyState;
+
+struct TZR_Config {
+ int _;
+ int width;
+ int height;
+ int scale;
+ int target_fps;
+ bool pixel_perfect;
+ bool show_cursor;
+ unsigned int mixer;
+ bool png_loading;
+ bool light_system;
+ const char *title;
+};
+
+struct TZR_Point {
+ int x;
+ int y;
+};
+
+struct TZR_Color {
+ int _;
+ float r;
+ float g;
+ float b;
+ float a;
+};
+
+struct TZR_Rect {
+ TZR_Point from;
+ TZR_Point to;
+};
+
+struct TZR_Raw {
+ void *data;
+ size_t size;
+};
+
+struct TZR_Image {
+ int width;
+ int height;
+ SDL_Texture *ptr;
+};
+
+struct TZR_Sound {
+ Mix_Chunk *ptr;
+};
+
+struct TZR_Resource {
+ TZR_ResourceType type;
+ char *path; /* allocated and freed by TZR; can be NULL */
+ long mtime;
+ union {
+ TZR_Raw raw; /* raw file data */
+ TZR_Image image;
+ TZR_Sound sound;
+ };
+};
+
+struct TZR_Event {
+ TZR_EventType type;
+ int button;
+};
+
+struct TZR_DrawImageArgs {
+ int _;
+ TZR_Uint id;
+ int x;
+ int y;
+ int ix;
+ int iy;
+ int w;
+ int h;
+ float r;
+ float sx;
+ float sy;
+ bool center;
+ bool flip_x;
+ bool flip_y;
+};
+
+struct TZR_DrawRectangleArgs {
+ int _;
+ int x;
+ int y;
+ int w;
+ int h;
+ bool fill;
+ bool center;
+};
+
+struct TZR_PlaySoundArgs {
+ int _;
+ int id;
+ int loop;
+ float volume;
+};
+/* headers/TZR_globals.h */
+
+extern TZR_Config ___tzr_config;
+extern TZR_Color ___tzr_color;
+extern TZR_Resource *___tzr_resources;
+extern size_t ___tzr_resources_capacity;
+extern size_t ___tzr_resources_size;
+extern SDL_Window *___tzr_window;
+extern SDL_Renderer *___tzr_renderer;
+extern SDL_Texture *___tzr_target;
+extern SDL_Texture *___tzr_target_light;
+extern unsigned long ___tzr_tick;
+extern unsigned long ___tzr_next_time;
+extern unsigned long ___tzr_min_dt;
+extern int ___tzr_should_quit;
+extern int ___tzr_mouse_x;
+extern int ___tzr_mouse_y;
+extern TZR_KeyState ___tzr_keystates[SDL_NUM_SCANCODES];
+extern TZR_KeyState ___tzr_mousestates[256];
+extern float ___tzr_scale;
+extern int ___tzr_off_x;
+extern int ___tzr_off_y;
+extern Mix_Music *___tzr_music;
+/* headers/TZR_resource.h */
+
+#define TZR_RES TZR_LoadResource
+
+/* Return 0 on error. */
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+TZR_Uint TZR_LoadResourceFromMemory(TZR_ResourceType type, const void *data, int size);
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+TZR_Uint TZR_LoadResourceTyped(TZR_ResourceType type, const char *path);
+
+/* Return 0 on error; try to autodetect resource type from file extension. */
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+TZR_Uint TZR_LoadResource(const char *path);
+
+/* Doesn't handle invalid ID. Pointer might get invalidated by resource
+ * creation calls, use with caution. */
+TZR_Resource *TZR_GetResourcePointer(TZR_Uint id);
+
+/* Doesn't handle invalid ID. Pointer might get invalidated by resource
+ * creation calls, use with caution. */
+TZR_Raw *TZR_GetRawResource(TZR_Uint id);
+
+/* Doesn't handle invalid ID. */
+TZR_ResourceType TZR_GetResourceType(TZR_Uint id);
+
+/* Doesn't handle invalid ID. */
+const char *TZR_GetResourcePath(TZR_Uint id);
+
+/* Doesn't handle invalid ID. */
+int TZR_GetImageWidth(TZR_Uint id);
+
+/* Doesn't handle invalid ID. */
+int TZR_GetImageHeight(TZR_Uint id);
+
+/* Reload ressource. Returns -1 on error and doesn't apply changes. */
+int TZR_ReloadResource(TZR_Uint id);
+
+/* Watch for changes on managed resources and hotreload if appropriate. */
+void TZR_ResourcesWatch(void);
+
+/* Used internaly. */
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+int TZR_DirectResourceLoad(TZR_Resource *res, const void *data, int size);
+void TZR_DestroyResource(TZR_Resource *res, int free_path);
+/* headers/TZR_sound.h */
+
+#define TZR_PlaySound(...) _TZR_PlaySound(&(const TZR_PlaySoundArgs){ \
+ .id=0, .loop=0, .volume=1.0f, ._=0, __VA_ARGS__ })
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int _TZR_PlaySound(const TZR_PlaySoundArgs *args);
+
+/* Return -1 on error. */
+int TZR_PlayMusic(const char *path, int loop);
+
+void TZR_StopMusic(void);
+/* headers/TZR_events.h */
+
+/* Write event data to `e`. Return 0 when event queue is empty. */
+int TZR_PollEvent(TZR_Event *e);
+
+/* Drain queued events with TZR_PollEvent and call TZR_ResourcesWatch. */
+void TZR_CycleEvents(void);
+
+void TZR_GetMousePosition(int *x, int *y);
+/* headers/TZR_render.h */
+
+/* All draw calls should happen between TZR_DrawBegin and TZR_DrawEnd.
+ * Return -1 on error. */
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+int TZR_DrawBegin(void);
+
+/* Return -1 on error. */
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+int TZR_DrawEnd(void);
+
+/* Return -1 on error. */
+#define TZR_DrawSetColor(...) _TZR_DrawSetColor(&(const TZR_Color){ \
+ .r=-1.0f, .g=-1.0f, .b=-1.0f, .a=-1.0f, ._=0, __VA_ARGS__ })
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int _TZR_DrawSetColor(const TZR_Color *color);
+
+/* Return -1 on error. */
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int TZR_DrawSetColor8(uint8_t r, uint8_t g, uint8_t b, uint8_t a);
+
+/* Return -1 on error. */
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int TZR_DrawClear(void);
+
+/* Return -1 on error. Draw point on `x`;`y` in the framebuffer. */
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int TZR_DrawPoint(int x, int y);
+
+/* Return -1 on error. Draw line between `x0`;`y0` and `x1`;`y1` in the
+ * framebuffer. */
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int TZR_DrawLine(int x0, int y0, int x1, int y1);
+
+/* Return -1 on error. Draw rectangle at `x`;`y` position of size `w`x`h` in the
+ * framebuffer. */
+#define TZR_DrawRectangle(...) _TZR_DrawRectangle( \
+ &(const TZR_DrawRectangleArgs){ \
+ .x=0, .y=0, .w=0, .h=0, .fill=false, .center=false, ._=0, __VA_ARGS__ })
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int _TZR_DrawRectangle(const TZR_DrawRectangleArgs *args);
+
+/* Return -1 on error. Draw texture ressource `id` at `x`;`y` position of
+ * the framebuffer. */
+#define TZR_DrawImage(...) _TZR_DrawImage(&(const TZR_DrawImageArgs){ \
+ .x=0, .y=0, .ix=0, .iy=0, .w=INT_MIN, .h=INT_MIN, .r=0.0f, .sx=1.0f, \
+ .sy=1.0f, .center=false, .flip_x=false, .flip_y=false, ._=0, \
+ __VA_ARGS__ })
+#ifdef TZR_PARANOID
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+#endif
+int _TZR_DrawImage(const TZR_DrawImageArgs *args);
+
+void TZR_ScreenTransform(int *x, int *y);
+/* headers/TZR_light.h */
+
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+int TZR_LightBegin(void);
+
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+int TZR_LightEnd(void);
+/* headers/TZR_keystate.h */
+
+TZR_KeyState TZR_GetKeyState(int scancode);
+bool TZR_IsKeyDown(int scancode);
+bool TZR_IsKeyUp(int scancode);
+bool TZR_IsKeyReleased(int scancode);
+bool TZR_IsKeyPressed(int scancode);
+/* headers/TZR.h */
+
+/* TZR manages all loaded resources internally and tries to avoid duplicate.
+ * A resource can be loaded multiple times if asked, identification doesn't rely
+ * on filepath. In doubt, use TZR_LoadResource ONCE per asset and store the ID.
+ * Resources are exposed as ID instead of pointer, as a future proof measure in
+ * case TZR needs to rearrange memory at runtime in the future. */
+
+#define TZR_Init(...) _TZR_Init(&(const TZR_Config){ \
+ .width=256, \
+ .height=224, \
+ .scale=2, \
+ .target_fps=60, \
+ .pixel_perfect=true, \
+ .show_cursor=false, \
+ .mixer=TZR_MIXER_ON, \
+ .png_loading=true, \
+ .light_system=false, \
+ .title="TZR", \
+ ._=0, __VA_ARGS__ })
+
+/* Should be called only once before usage of any other function unless
+ * otherwise specified. On error this calls TZR_Quit and returns -1.
+ * `config` will be duplicated internally and can be safely discarded. */
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+int _TZR_Init(const TZR_Config *config);
+
+/* Destroy and free everything created by TZR. */
+void TZR_Quit(void);
+
+/* Return non-zero value when a quit event has been received. */
+int TZR_ShouldQuit(void);
+
+int TZR_MainLoop(int (*main_loop)(void *udata), void *udata);
+
+unsigned long TZR_GetTick(void);
+
+/*
+** Copyright (c) 2023 kdx
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+*/
+/* commit hash: a4112e4311eb226c4f43cb697aa7f6c34bd7b9a5 */
diff --git a/src/camera.c b/src/camera.c
new file mode 100644
index 0000000..9dd95da
--- /dev/null
+++ b/src/camera.c
@@ -0,0 +1,48 @@
+#include "camera.h"
+#include "cfg.h"
+#include "clamp.h"
+#include "map.h"
+#include <stdlib.h>
+
+static double cam[2] = {0, 0};
+static double off[2] = {0, 0};
+
+int shake = 0;
+
+void
+camera_init(void)
+{
+ cam[0] = 0.0;
+ cam[1] = 0.0;
+}
+
+void
+camera_teleport(int x, int y)
+{
+ cam[0] = x;
+ cam[1] = y;
+}
+
+void
+camera_update(double dest[2])
+{
+ for (int i = 0; i < 2; i++)
+ cam[i] += 0.1 * (dest[i] - cam[i]);
+ cam[0] = clamp(DWIDTH/2.0, map_width() * TSIZE - DWIDTH/2.0, cam[0]);
+ cam[1] = clamp(DHEIGHT/2.0, map_height() * TSIZE - DHEIGHT/2.0, cam[1]);
+ off[0] = shake ? (2 - rand() % 5) : 0;
+ off[1] = shake ? (2 - rand() % 5) : 0;
+ shake -= (shake > 0);
+}
+
+int
+camera_x(double scale)
+{
+ return -cam[0] * scale + DWIDTH / 2.0 + off[0];
+}
+
+int
+camera_y(double scale)
+{
+ return -cam[1] * scale + DHEIGHT / 2.0 + off[1];
+}
diff --git a/src/camera.h b/src/camera.h
new file mode 100644
index 0000000..8c572af
--- /dev/null
+++ b/src/camera.h
@@ -0,0 +1,7 @@
+#pragma once
+
+void camera_init(void);
+void camera_teleport(int x, int y);
+void camera_update(double dest[2]);
+int camera_x(double scale);
+int camera_y(double scale);
diff --git a/src/cfg.h b/src/cfg.h
new file mode 100644
index 0000000..97d457b
--- /dev/null
+++ b/src/cfg.h
@@ -0,0 +1,7 @@
+#pragma once
+#include "px.h"
+
+#define TARGET_FPS 30
+#define TSIZE 16
+#define DWIDTH PX_WIDTH
+#define DHEIGHT PX_HEIGHT
diff --git a/src/clamp.h b/src/clamp.h
new file mode 100644
index 0000000..518a237
--- /dev/null
+++ b/src/clamp.h
@@ -0,0 +1,19 @@
+#pragma once
+
+static double
+dmin(double a, double b)
+{
+ return (a < b) ? a : b;
+}
+
+static double
+dmax(double a, double b)
+{
+ return (a > b) ? a : b;
+}
+
+static double
+clamp(double min, double max, double v)
+{
+ return dmax(min, dmin(max, v));
+}
diff --git a/src/entity.c b/src/entity.c
new file mode 100644
index 0000000..c3d65b7
--- /dev/null
+++ b/src/entity.c
@@ -0,0 +1,174 @@
+#include "entity.h"
+#include "entitytag.h"
+#include "map.h"
+#include "game.h"
+#include "tileset.h"
+#include "cfg.h"
+#include <stdio.h>
+#include <strings.h>
+
+static void
+_points(Entity *this, int ox, int oy, int *x0, int *x1, int *y0, int *y1)
+{
+ *x0 = this->pos[0] - this->width / 2 + ox;
+ *y0 = this->pos[1] - this->height / 2 + oy;
+ *x1 = *x0 + this->width - 1;
+ *y1 = *y0 + this->height - 1;
+}
+
+unsigned int
+entity_type(const char *typename)
+{
+ if (typename == NULL)
+ return 0;
+ for (unsigned i = 0; i < num_entitytags; i++)
+ if (strcasecmp(typename, entitytags[i].name) == 0)
+ return i + 1;
+ printf("unknown type '%s'\n", typename);
+ return 0;
+}
+
+unsigned int
+entity_type_parent(unsigned int type)
+{
+ if (type == 0)
+ return 0;
+ if (entitytags[type - 1].parent != NULL)
+ return entity_type(entitytags[type - 1].parent);
+ return 0;
+}
+
+bool
+entity_is_type(unsigned int type, unsigned int search)
+{
+ do {
+ if (type == search)
+ return true;
+ type = entity_type_parent(type);
+ } while (type != 0);
+ return false;
+}
+
+static bool
+_entity_collide(Entity *this, int ox, int oy, unsigned int type)
+{
+ int x0, y0, x1, y1;
+ _points(this, ox, oy, &x0, &x1, &y0, &y1);
+ for (int y = y0; y <= y1; y++)
+ for (int x = x0; x <= x1; x++)
+ if (map_get_px(x, y, type))
+ return true;
+ return false;
+}
+
+bool
+entity_collide(Entity *this, int ox, int oy)
+{
+ return _entity_collide(this, ox, oy, TILE_SOLID);
+}
+
+static bool
+collide_smol_spike(Entity *this, int ox, int oy)
+{
+ const int width = this->width;
+ const int height = this->height;
+ this->width = 2;
+ this->height = 2;
+ const bool collided = _entity_collide(this, ox, oy, TILE_SMOLSPIKE);
+ this->width = width;
+ this->height = height;
+ return collided;
+}
+
+bool
+entity_collide_spike(Entity *this, int ox, int oy)
+{
+ return (_entity_collide(this, ox, oy, TILE_SPIKE) ||
+ collide_smol_spike(this, ox, oy));
+}
+
+bool
+entity_oob(Entity *this)
+{
+ return (this->pos[0] + this->width / 2 <= 0 ||
+ this->pos[1] + this->height / 2 <= 0 ||
+ this->pos[0] - this->width / 2 >= map_width() * TSIZE ||
+ this->pos[1] - this->height / 2 >= map_height() * TSIZE);
+}
+
+bool
+entity_meet(Entity *this, Entity *other)
+{
+ int tx0, ty0, tx1, ty1;
+ int ox0, oy0, ox1, oy1;
+ _points(this, 0, 0, &tx0, &tx1, &ty0, &ty1);
+ _points(other, 0, 0, &ox0, &ox1, &oy0, &oy1);
+ return (((tx0 < ox1 && tx1 > ox0) || (tx0 > ox1 && tx1 < ox0)) &&
+ ((ty0 < oy1 && ty1 > oy0) || (ty0 > oy1 && ty1 < oy0)));
+}
+
+Entity *
+entity_place_meeting(Entity *this, struct Game *g, unsigned int type)
+{
+ for (int i = MAX_ENTITIES - 1; i >= 0; i--)
+ if (this != &g->entities[i] &&
+ entity_is_type(g->entities[i].type, type) &&
+ entity_meet(this, &g->entities[i]))
+ return &g->entities[i];
+ return NULL;
+}
+
+#define MOVE_COLLIDE (entity_collide(this, 0, 0) || \
+ (this->solid_spikes && entity_collide_spike(this, 0, 0)))
+
+void
+entity_move(Entity *this, struct Game *g)
+{
+ (void)g;
+
+ if (this->ignore_solids) {
+ for (int a = 0; a < 2; a++) {
+ const double sum = this->vel[a] + this->rem[a];
+ int spd = (int)sum;
+ this->rem[a] = sum - spd;
+ this->pos[a] += spd;
+ }
+ return;
+ }
+ if (MOVE_COLLIDE) {
+ this->vel[0] = 0.0;
+ this->vel[1] = 0.0;
+ this->rem[0] = 0.0;
+ this->rem[1] = 0.0;
+ return;
+ }
+ for (int a = 0; a < 2; a++) {
+ const double sum = this->vel[a] + this->rem[a];
+ int spd = (int)sum;
+ this->rem[a] = sum - spd;
+ const int sign = (spd > 0) - (spd < 0);
+ if (sign == 0)
+ continue;
+ while (spd != 0) {
+ this->pos[a] += sign;
+ if (MOVE_COLLIDE) {
+ this->pos[a] -= sign;
+ this->rem[a] = 0.0;
+ if (a == 0 && this->type == entity_type("player"))
+ this->vel[a] *= 0.9;
+ else
+ this->vel[a] = 0.0;
+ break;
+ }
+ spd -= sign;
+ }
+ }
+}
+
+Entity *
+entity_init(Entity *this, Game *g, unsigned int type, const char *name, int x,
+ int y, int w, int h)
+{
+ entitytags[type - 1].init(this, g, name, x, y, w, h);
+ return this;
+}
diff --git a/src/entity.h b/src/entity.h
new file mode 100644
index 0000000..b41b560
--- /dev/null
+++ b/src/entity.h
@@ -0,0 +1,69 @@
+#pragma once
+#include <stdbool.h>
+#include "TZR.h"
+
+struct Game;
+
+typedef struct Entity Entity;
+struct Entity {
+ unsigned long uuid;
+ void (*update_begin)(Entity *this, struct Game *g);
+ void (*update)(Entity *this, struct Game *g);
+ void (*update_end)(Entity *this, struct Game *g);
+ void (*draw_begin)(Entity *this, struct Game *g);
+ void (*draw)(Entity *this, struct Game *g);
+ void (*draw_end)(Entity *this, struct Game *g);
+ void (*draw_ui)(Entity *this, struct Game *g);
+ void (*draw_last)(Entity *this, struct Game *g);
+ void (*light)(Entity *this, struct Game *g);
+ unsigned int type;
+ char name[256];
+ int pos[2];
+ int prev_pos[2];
+ double vel[2];
+ double rem[2];
+ int gravity[2];
+ int width;
+ int height;
+ bool ignore_solids;
+ bool solid_spikes;
+ bool pusher;
+ bool solid;
+ float angle;
+ float target_angle;
+ float a;
+ int life;
+ bool hold;
+ int hold_duration;
+ float frame;
+ bool was_on_ground;
+ int spr;
+ unsigned long next;
+ int hurt;
+ char time[16];
+ int dirx;
+ bool on_ground;
+ bool air_dash;
+ int air_state;
+ int anim_state;
+ int anim_life;
+ int dead;
+ unsigned long attached;
+ int grace;
+ int jump_buffer;
+ int max_pizza;
+ union {
+ };
+};
+
+unsigned int entity_type(const char *typename);
+unsigned int entity_type_parent(unsigned int type);
+bool entity_is_type(unsigned int type, unsigned int search);
+bool entity_collide(Entity *this, int ox, int oy);
+bool entity_collide_spike(Entity *this, int ox, int oy);
+bool entity_oob(Entity *this);
+bool entity_meet(Entity *this, Entity *other);
+Entity *entity_place_meeting(Entity *this, struct Game *g, unsigned int type);
+void entity_move(Entity *this, struct Game *g);
+Entity *entity_init(Entity *this, struct Game *g, unsigned int type,
+ const char *name, int x, int y, int w, int h);
diff --git a/src/entityimpl.h b/src/entityimpl.h
new file mode 100644
index 0000000..a8b0f75
--- /dev/null
+++ b/src/entityimpl.h
@@ -0,0 +1,72 @@
+#pragma once
+#include "game.h"
+#include "cfg.h"
+#include "TZR.h"
+#include "entitytag.h"
+#include "camera.h"
+#include <string.h>
+
+#define IMPL_UNUSED (void)this, (void)g
+#define UNPACK_POS this->pos[0], this->pos[1]
+#define UNPACK_SIZE this->width, this->height
+
+static void *_draw_begin;
+static void *_draw;
+static void *_draw_end;
+static void *_draw_ui;
+static void *_draw_last;
+static void *_update_begin;
+static void *_update;
+static void *_update_end;
+static void *_light;
+
+__attribute__((constructor)) static void silence_unused(void) {
+ (void)_draw_begin;
+ (void)_draw;
+ (void)_draw_end;
+ (void)_draw_ui;
+ (void)_draw_last;
+ (void)_update_begin;
+ (void)_update;
+ (void)_update_end;
+ (void)_light;
+}
+
+#define IMPL(X) static void X(Entity *this, Game *g); \
+__attribute__((constructor)) static void init_##X() { _##X = X; } \
+static void X(Entity *this, Game *g)
+
+#define IMPL_INIT(X,P) static void init(Entity *, Game *, const char *, \
+ int, int, int, int); \
+__attribute__((constructor)) static void init_tag(void) { \
+ entitytags[num_entitytags].init = init; \
+ entitytags[num_entitytags].parent = P; \
+ entitytags[num_entitytags++].name = #X; \
+} \
+static void _init(Entity *this, Game *g); \
+static void init(Entity *this, Game *g, const char *name, int x, int y, int w, int h) { \
+ { \
+ const __auto_type uuid = this->uuid; \
+ memset(this, 0, sizeof(*this)); \
+ this->uuid = uuid; \
+ } \
+ this->update_begin = _update_begin; \
+ this->update = _update; \
+ this->update_end = _update_end; \
+ this->draw_begin = _draw_begin; \
+ this->draw = _draw; \
+ this->draw_end = _draw_end; \
+ this->draw_ui = _draw_ui; \
+ this->draw_last = _draw_last; \
+ this->light = _light; \
+ if (name != NULL) strncpy(this->name, name, sizeof(this->name) - 1); \
+ this->pos[0] = x; \
+ this->pos[1] = y; \
+ this->width = w; \
+ this->height = h; \
+ this->type = entity_type(#X); \
+ this->solid = false; \
+ this->pusher = false; \
+ _init(this, g); \
+} \
+static void _init(Entity *this, Game *g)
diff --git a/src/entitytag.c b/src/entitytag.c
new file mode 100644
index 0000000..de56455
--- /dev/null
+++ b/src/entitytag.c
@@ -0,0 +1,4 @@
+#include "entitytag.h"
+
+unsigned int num_entitytags = 0;
+EntityTag entitytags[256] = {};
diff --git a/src/entitytag.h b/src/entitytag.h
new file mode 100644
index 0000000..7c9a972
--- /dev/null
+++ b/src/entitytag.h
@@ -0,0 +1,11 @@
+#pragma once
+#include "game.h"
+
+typedef struct {
+ const char *name;
+ const char *parent;
+ void (*init)(Entity*, Game*, const char*, int x, int y, int w, int h);
+} EntityTag;
+
+extern unsigned int num_entitytags;
+extern EntityTag entitytags[256];
diff --git a/src/game.c b/src/game.c
new file mode 100644
index 0000000..1a8a246
--- /dev/null
+++ b/src/game.c
@@ -0,0 +1,182 @@
+#include "game.h"
+#include "cfg.h"
+#include "entity.h"
+#include "map.h"
+#include "camera.h"
+#include <stdio.h>
+#include <string.h>
+
+int
+game_init(Game *this)
+{
+ map_reset();
+ memset(this, 0, sizeof(*this));
+ camera_init();
+ game_restart_scene(this);
+ return 0;
+}
+
+void
+game_update(Game *this)
+{
+ this->timer += 1;
+ if (this->queue_next_scene) {
+ if (--this->queue_next_scene == 0) {
+ this->queue_restart_scene = 0;
+ this->queue_previous_scene = 0;
+ this->checkpoint = false;
+ map_next();
+ this->missed_bois +=
+ game_entity_count(this, entity_type("boi"));
+ game_restart_scene(this);
+ this->total_bois +=
+ game_entity_count(this, entity_type("boi"));
+ return;
+ }
+ }
+ if (this->queue_previous_scene) {
+ if (--this->queue_next_scene == 0) {
+ this->queue_restart_scene = 0;
+ this->queue_next_scene = 0;
+ map_previous();
+ game_restart_scene(this);
+ return;
+ }
+ }
+ if (this->queue_restart_scene) {
+ if (--this->queue_restart_scene == 0)
+ game_restart_scene(this);
+ }
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->update_begin != NULL)
+ e->update_begin(e, this);
+ }
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->update != NULL)
+ e->update(e, this);
+ }
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->update_end != NULL)
+ e->update_end(e, this);
+ }
+
+ // tidy
+ for (int i = 1; i < MAX_ENTITIES; i++) {
+ Entity *const e_prev = &this->entities[i - 1];
+ Entity *const e = &this->entities[i];
+ if (e->type && !e_prev->type) {
+ memcpy(e_prev, e, sizeof(*e));
+ e->type = 0;
+ }
+ }
+}
+
+void
+game_light(Game *this)
+{
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->light != NULL)
+ e->light(e, this);
+ }
+}
+
+void
+game_draw(Game *this)
+{
+ map_draw();
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->draw_begin != NULL)
+ e->draw_begin(e, this);
+ }
+ for (int i = MAX_ENTITIES - 1; i >= 0; i--) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->draw != NULL)
+ e->draw(e, this);
+ }
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->draw_end != NULL)
+ e->draw_end(e, this);
+ }
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->draw_last != NULL)
+ e->draw_last(e, this);
+ }
+ map_draw_foreground();
+ if (this->show_ui <= 0)
+ return;
+ this->show_ui -= 1;
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ Entity *const e = &this->entities[i];
+ if (e->type != 0 && e->draw_ui != NULL)
+ e->draw_ui(e, this);
+ }
+}
+
+void
+game_restart_scene(Game *this)
+{
+ memset(this->entities, 0, sizeof(this->entities));
+ this->icheckpoint = 0;
+ unsigned int size;
+ const Tiled2cObject *const objects = map_objects(&size);
+ for (unsigned i = 0u; i < size; i++) {
+ const Tiled2cObject *const object = &objects[i];
+ const unsigned int type = (object->type && object->type[0])
+ ? entity_type(object->type)
+ : entity_type(object->name);
+ if (type == 0)
+ continue;
+ const double x = object->x + object->width / 2;
+ const double y = object->y + object->height / 2;
+ if (y < 0 || y >= map_height() * TSIZE)
+ continue;
+ entity_init(game_create_entity(this), this, type, object->name,
+ x, y, object->width, object->height);
+ }
+}
+
+Entity *
+game_create_entity(Game *this)
+{
+ Entity *e = &this->entities[MAX_ENTITIES - 1];
+ for (int i = 0; i < MAX_ENTITIES; i++)
+ if (this->entities[i].type == 0) {
+ e = &this->entities[i];
+ break;
+ }
+ e->uuid = this->uuid++;
+ return e;
+}
+
+int
+game_entity_count(Game *this, unsigned int type)
+{
+ int count = 0;
+ for (int i = 0; i < MAX_ENTITIES; i++)
+ count += (this->entities[i].type == type);
+ return count;
+}
+
+Entity *
+game_get_entity(Game *this, unsigned int type)
+{
+ for (int i = 0; i < MAX_ENTITIES; i++)
+ if (this->entities[i].type == type)
+ return &this->entities[i];
+ return NULL;
+}
+
+Entity *
+game_get_entity_by_uuid(Game *this, unsigned long uuid) {
+ for (int i = 0; i < MAX_ENTITIES; i++)
+ if (this->entities[i].uuid == uuid)
+ return &this->entities[i];
+ return NULL;
+}
diff --git a/src/game.h b/src/game.h
new file mode 100644
index 0000000..f2d1928
--- /dev/null
+++ b/src/game.h
@@ -0,0 +1,36 @@
+#pragma once
+#include "entity.h"
+
+enum { MAX_ENTITIES = 1024 };
+
+typedef struct Game {
+ unsigned long uuid;
+ unsigned long timer;
+ int queue_next_scene;
+ int queue_previous_scene;
+ int queue_restart_scene;
+ int show_ui;
+ int missed_bois;
+ int total_bois;
+ int deaths;
+ int dashes;
+ int icheckpoint;
+ int checkpoint;
+ int checkpoint_pos[2];
+ int bois[3];
+ int bois_count;
+ Entity entities[MAX_ENTITIES];
+} Game;
+
+#if __STDC_VERSION__ > 201710L
+[[nodiscard]]
+#endif
+int game_init(Game *this);
+void game_update(Game *this);
+void game_light(Game *this);
+void game_draw(Game *this);
+void game_restart_scene(Game *this);
+Entity *game_create_entity(Game *this);
+int game_entity_count(Game *this, unsigned int type);
+Entity *game_get_entity(Game *this, unsigned int type);
+Entity *game_get_entity_by_uuid(Game *this, unsigned long type);
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..388fb02
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,88 @@
+#include "TZR.h"
+#include "cfg.h"
+#include "game.h"
+#include "map.h"
+#include "tileset.h"
+#include <SDL2/SDL_video.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <SDL2/SDL_main.h>
+#include <time.h>
+
+static Game *game = NULL;
+
+static void deinit(void);
+static int main_loop(void *udata);
+
+int
+main(int argc, char **argv)
+{
+ (void)argc, (void)argv;
+
+ if (TZR_Init(.width=DWIDTH,
+ .height=DHEIGHT,
+ .scale=2,
+ .target_fps=TARGET_FPS,
+ .pixel_perfect=true,
+ .mixer=false,
+ .png_loading=false,
+ .light_system=false,
+ .title="goty of the day"))
+ return 1;
+ srand(time(NULL));
+
+ Mix_AllocateChannels(32);
+
+ if (atexit(deinit)) {
+ perror("main:atexit");
+ deinit();
+ return 1;
+ }
+
+ if ((game = malloc(sizeof(*game))) == NULL) {
+ perror("main:malloc");
+ return 1;
+ }
+ if (game_init(game))
+ return 1;
+
+ return TZR_MainLoop(main_loop, game);
+}
+
+static void
+deinit(void)
+{
+ if (game != NULL)
+ free(game);
+ TZR_Quit();
+}
+
+static int
+main_loop(void *udata)
+{
+ Game *const game = udata;
+
+ game_update(game);
+ tileset_update();
+
+ if (TZR_IsKeyPressed(SDL_SCANCODE_R)) {
+ if (game_init(game))
+ return 1;
+ }
+ if (TZR_IsKeyPressed(SDL_SCANCODE_F)) {
+ static bool fullscreen = false;
+ fullscreen = !fullscreen;
+ SDL_SetWindowFullscreen(___tzr_window, fullscreen);
+ }
+ if (TZR_IsKeyPressed(SDL_SCANCODE_N))
+ game->queue_next_scene = 1;
+
+ if (TZR_DrawBegin())
+ return 1;
+ TZR_DrawSetColor8(0x03, 0x06, 0x16, 0xff);
+ TZR_DrawClear();
+ game_draw(game);
+ if (TZR_DrawEnd())
+ return 1;
+ return 0;
+}
diff --git a/src/map.c b/src/map.c
new file mode 100644
index 0000000..311db7f
--- /dev/null
+++ b/src/map.c
@@ -0,0 +1,130 @@
+#include "map.h"
+#include "cfg.h"
+#include "TZR.h"
+#include "tiled2c.h"
+#include "tileset.h"
+#include "camera.h"
+#include <stddef.h>
+
+extern const Tiled2cMap embed_game_tmj;
+extern const Tiled2cMap embed_grablmao_tmj;
+extern const Tiled2cMap embed_pizzeria_tmj;
+extern const Tiled2cMap embed_banger_tmj;
+extern const Tiled2cMap embed_end_tmj;
+
+static const Tiled2cMap *const maps[] = {
+ &embed_game_tmj,
+ NULL
+};
+static unsigned int map_id;
+
+void
+map_reset(void)
+{
+ map_id = 0;
+}
+
+void
+map_next(void)
+{
+ map_id += (maps[map_id + 1] != NULL);
+}
+
+void
+map_previous(void)
+{
+ if (map_id > 0)
+ map_id -= 1;
+}
+
+int
+map_width(void)
+{
+ return maps[map_id]->width;
+}
+
+int
+map_height(void)
+{
+ return maps[map_id]->height;
+}
+
+bool
+map_get(int x, int y, unsigned int type)
+{
+ type &= ~TILED_TRANSFORMS;
+ const Tiled2cMap *const map = maps[map_id];
+ if (x >= map_width() || y < 0 || y >= map_height())
+ return 0;
+ if (x < 0)
+ return (type != TILE_SPIKE);
+ if (map->numlayers < 1)
+ return (type != TILE_SPIKE);
+ for (unsigned int i = 0; i < map->numlayers; i++) {
+ const unsigned int tile = map->layers[i]
+ .data[x + y * map_width()];
+ if (tile_type(tile) == type)
+ return true;
+ }
+ return false;
+}
+
+bool
+map_get_px(int x, int y, unsigned int type)
+{
+ if (y < 0)
+ return 0;
+ if (x < 0)
+ return (type != TILE_SPIKE);
+ return map_get(x / TSIZE, y / TSIZE, type);
+}
+
+static void
+draw_layer(const Tiled2cLayer *layer)
+{
+ const int tset = TZR_RES("res/tset.bmp");
+ const int tset_w = TZR_GetImageWidth(tset);
+ const int ox = camera_x(layer->parallaxx);
+ const int oy = camera_y(layer->parallaxy);
+ for (int y = 0; y < map_height(); y++)
+ for (int x = 0; x < map_width(); x++) {
+ const unsigned tile =
+ layer->data[x + y * map_width()];
+ if (tile == 0)
+ continue;
+ const unsigned vtile = tile_visual(tile);
+ const int dx = ox + x * TSIZE;
+ const int dy = oy + y * TSIZE;
+ const int ix = vtile % (tset_w / TSIZE) * TSIZE;
+ const int iy = vtile / (tset_w / TSIZE) * TSIZE;
+ TZR_DrawImage(tset, dx, dy, ix, iy, TSIZE, TSIZE);
+ }
+}
+
+void
+map_draw(void)
+{
+ const Tiled2cMap *const map = maps[map_id];
+ TZR_DrawSetColor(1, 1, 1);
+ for (unsigned int i = 0; i < map->numlayers; i++)
+ if (map->layers[i].visible && map->layers[i].parallaxx <= 1.0)
+ draw_layer(&map->layers[i]);
+}
+
+void
+map_draw_foreground(void)
+{
+ const Tiled2cMap *const map = maps[map_id];
+ TZR_DrawSetColor(1, 1, 1);
+ for (unsigned int i = 0; i < map->numlayers; i++)
+ if (map->layers[i].visible && map->layers[i].parallaxx > 1.0)
+ draw_layer(&map->layers[i]);
+}
+
+const Tiled2cObject *
+map_objects(unsigned int *size)
+{
+ if (size != NULL)
+ *size = maps[map_id]->numobjects;
+ return maps[map_id]->objects;
+}
diff --git a/src/map.h b/src/map.h
new file mode 100644
index 0000000..a76930c
--- /dev/null
+++ b/src/map.h
@@ -0,0 +1,21 @@
+#pragma once
+#include "tiled2c.h"
+#include <stdbool.h>
+
+enum {
+ TILED_FLIP_X = 1 << 31,
+ TILED_FLIP_Y = 1 << 30,
+ TILED_TRANSFORMS = TILED_FLIP_X | TILED_FLIP_Y,
+};
+
+void map_reset(void);
+void map_next(void);
+void map_previous(void);
+int map_width(void);
+int map_height(void);
+int map_room_y(void);
+bool map_get(int x, int y, unsigned int type);
+bool map_get_px(int x, int y, unsigned int type);
+void map_draw(void);
+void map_draw_foreground(void);
+const Tiled2cObject *map_objects(unsigned int *size);
diff --git a/src/player.c b/src/player.c
new file mode 100644
index 0000000..d98bdee
--- /dev/null
+++ b/src/player.c
@@ -0,0 +1,29 @@
+#include "entityimpl.h"
+
+IMPL_INIT(player, 0) {
+ IMPL_UNUSED;
+}
+
+IMPL(update) {
+ IMPL_UNUSED;
+ const int kleft = TZR_IsKeyDown(SDL_SCANCODE_A) ||
+ TZR_IsKeyDown(SDL_SCANCODE_LEFT);
+ const int kright = TZR_IsKeyDown(SDL_SCANCODE_D) ||
+ TZR_IsKeyDown(SDL_SCANCODE_RIGHT);
+ const int kpjump = TZR_IsKeyPressed(SDL_SCANCODE_Z) ||
+ TZR_IsKeyPressed(SDL_SCANCODE_UP) ||
+ TZR_IsKeyPressed(SDL_SCANCODE_SPACE);
+ const int dirx = kright - kleft;
+
+ this->vel[0] = dirx * 2;
+
+ entity_move(this, g);
+ camera_update(this->vel);
+}
+
+IMPL(draw) {
+ IMPL_UNUSED;
+ TZR_DrawSetColor(1, 1, 1);
+ TZR_DrawRectangle(this->pos[0], this->pos[1], this->width, this->height,
+ .center=true);
+}
diff --git a/src/px.c b/src/px.c
new file mode 100644
index 0000000..5c1af9b
--- /dev/null
+++ b/src/px.c
@@ -0,0 +1,260 @@
+/* Licensing information can be found at the end of the file. */
+#include "px.h"
+#include "TZR.h"
+#include <assert.h>
+
+PxCol pxbuf[PX_WIDTH * PX_HEIGHT] = {0};
+PxPal pxpal[256] = {
+ /* PICO-8 palette */
+ {0x00, 0x00, 0x00, false, 0, 0},
+ {0x1d, 0x2b, 0x53, false, 0, 0},
+ {0x7e, 0x25, 0x53, false, 0, 0},
+ {0x00, 0x87, 0x51, false, 0, 0},
+ {0xab, 0x52, 0x36, false, 0, 0},
+ {0x5f, 0x57, 0x4f, false, 0, 0},
+ {0xc2, 0xc3, 0xc7, false, 0, 0},
+ {0xff, 0xf1, 0xe8, false, 0, 0},
+ {0xff, 0x00, 0x4d, false, 0, 0},
+ {0xff, 0xa3, 0x00, false, 0, 0},
+ {0xff, 0xec, 0x27, false, 0, 0},
+ {0x00, 0xe4, 0x36, false, 0, 0},
+ {0x29, 0xad, 0xff, false, 0, 0},
+ {0x83, 0x76, 0x9c, false, 0, 0},
+ {0xff, 0x77, 0xa8, false, 0, 0},
+ {0xff, 0xcc, 0xaa, false, 0, 0},
+};
+static struct {
+ int x;
+ int y;
+ int w;
+ int h;
+} pxclip = {0, 0, PX_WIDTH, PX_HEIGHT};
+static SDL_Texture *pxtexture = NULL;
+
+int
+pxInit(void)
+{
+ pxtexture = SDL_CreateTexture(___tzr_renderer,
+ SDL_PIXELFORMAT_ARGB8888,
+ SDL_TEXTUREACCESS_STREAMING,
+ PX_WIDTH, PX_HEIGHT);
+ if (pxtexture == NULL) {
+ fprintf(stderr, "%s\n", SDL_GetError());
+ return 1;
+ }
+ return 0;
+}
+
+void
+pxDeinit(void)
+{
+ if (pxtexture != NULL) {
+ SDL_DestroyTexture(pxtexture);
+ pxtexture = NULL;
+ }
+}
+
+void
+pxFlip(void)
+{
+ uint8_t *pixels = NULL;
+ int pitch = 0;
+ SDL_LockTexture(pxtexture, NULL, (void **)&pixels, &pitch);
+
+ for (int y = 0; y < PX_HEIGHT; y++) {
+ for (int x = 0; x < PX_WIDTH; x++) {
+ const int ix = x << 2;
+ PxCol col = pxbuf[x + y * PX_WIDTH];
+ if (pxpal[col].spal)
+ col = pxpal[col].spal - 1;
+ pixels[ix + y * pitch + 0] = pxpal[col].b;
+ pixels[ix + y * pitch + 1] = pxpal[col].g;
+ pixels[ix + y * pitch + 2] = pxpal[col].r;
+ pixels[ix + y * pitch + 3] = 255;
+ }
+ }
+
+ SDL_UnlockTexture(pxtexture);
+ SDL_RenderCopy(___tzr_renderer, pxtexture, NULL, NULL);
+}
+
+void
+pxCls(PxCol c)
+{
+ for (int y = 0; y < PX_HEIGHT; y++)
+ for (int x = 0; x < PX_WIDTH; x++)
+ pxPset(x, y, c);
+}
+
+void
+pxClip(int x, int y, int w, int h)
+{
+ pxclip.x = pxClamp(0, PX_WIDTH - 1, x);
+ pxclip.y = pxClamp(0, PX_HEIGHT - 1, y);
+ pxclip.w = pxClamp(0, PX_WIDTH - pxclip.x - 1, w);
+ pxclip.h = pxClamp(0, PX_HEIGHT - pxclip.y - 1, h);
+}
+
+void
+pxClipReset(void)
+{
+ pxClip(0, 0, PX_WIDTH, PX_HEIGHT);
+}
+
+void
+pxPset(int x, int y, PxCol c)
+{
+ if (x < pxclip.x || x >= pxclip.x + pxclip.w ||
+ y < pxclip.y || y >= pxclip.y + pxclip.h)
+ return;
+ if (pxpal[c].pal)
+ c = pxpal[c].pal - 1;
+ pxbuf[x + y * PX_WIDTH] = c;
+}
+
+uint8_t
+pxPget(int x, int y)
+{
+ if (x < 0 || y < 0 || x >= PX_WIDTH || y >= PX_HEIGHT)
+ return 0;
+ return pxbuf[x + y * PX_WIDTH];
+}
+
+void
+pxRect(int x0, int y0, int x1, int y1, PxCol col)
+{
+ if (x0 > x1) {
+ x0 ^= x1;
+ x1 ^= x0;
+ x0 ^= x1;
+ }
+ if (y0 > y1) {
+ y0 ^= y1;
+ y1 ^= y0;
+ y0 ^= y1;
+ }
+ for (int x = x0; x <= x1; x++) {
+ pxPset(x, y0, col);
+ pxPset(x, y1, col);
+ }
+ for (int y = y0; y <= y1; y++) {
+ pxPset(x0, y, col);
+ pxPset(x1, y, col);
+ }
+}
+
+void
+pxRectfill(int x0, int y0, int x1, int y1, PxCol col)
+{
+ if (x0 > x1) {
+ x0 ^= x1;
+ x1 ^= x0;
+ x0 ^= x1;
+ }
+ if (y0 > y1) {
+ y0 ^= y1;
+ y1 ^= y0;
+ y0 ^= y1;
+ }
+ for (int y = y0; y <= y1; y++)
+ for (int x = x0; x <= x1; x++)
+ pxPset(x, y, col);
+}
+
+void
+_pxSpr(const PxSprArgs *args)
+{
+ int w = (args->w < 0) ? (int)(args->spr->w) : (args->w);
+ w = pxMin(w, args->spr->w - args->ix);
+
+ int h = (args->h < 0) ? (int)(args->spr->h) : (args->h);
+ h = pxMin(h, args->spr->h - args->iy);
+
+ for (int y = 0; y < h; y++) {
+ for (int x = 0; x < w; x++) {
+ const int dx = x + args->x;
+ const int dy = y + args->y;
+ int ix = args->ix + ((args->flip_x)
+ ? (w - x - 1)
+ : (x));
+ int iy = args->iy + ((args->flip_y)
+ ? (h - y - 1)
+ : (y));
+ const PxCol c = args->spr->data[ix + iy * args->spr->w];
+ if (!pxpal[c].t)
+ pxPset(dx, dy, c);
+ }
+ }
+}
+
+void
+_pxPal(const PxPalArgs *args)
+{
+ assert((args->c0 == -1) ^ (args->c1 != -1));
+ if (args->c0 == -1)
+ for (int i = 0; i < 256; i++)
+ pxpal[i].pal = 0;
+ else
+ pxpal[args->c0].pal = args->c1 + 1;
+}
+
+void
+_pxSpal(const PxSpalArgs *args)
+{
+ assert((args->c0 == -1) ^ (args->c1 != -1));
+ if (args->c0 == -1)
+ for (int i = 0; i < 256; i++)
+ pxpal[i].spal = 0;
+ else
+ pxpal[args->c0].spal = args->c1 + 1;
+}
+
+void
+_pxPalt(const PxPaltArgs *args)
+{
+ if (args->col < 0 || args->col > 255)
+ for (int i = 0; i < 256; i++)
+ pxpal[i].t = false;
+ else
+ pxpal[args->col].t = args->t;
+}
+
+int
+pxMin(int a, int b)
+{
+ return (a < b) ? a : b;
+}
+
+int
+pxMax(int a, int b)
+{
+ return (a > b) ? a : b;
+}
+
+int
+pxClamp(int min, int max, int v)
+{
+ return pxMax(min, pxMin(max, v));
+}
+
+/*
+** Copyright (c) 2023 kdx
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+*/
diff --git a/src/px.h b/src/px.h
new file mode 100644
index 0000000..f413bdc
--- /dev/null
+++ b/src/px.h
@@ -0,0 +1,132 @@
+/* Licensing information can be found at the end of the file. */
+#pragma once
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifndef PX_WIDTH
+#define PX_WIDTH 128
+#endif
+#ifndef PX_HEIGHT
+#define PX_HEIGHT 128
+#endif
+
+typedef uint8_t PxCol;
+
+typedef struct {
+ uint8_t r, g, b;
+ bool t; /* tranparency */
+ int pal, spal;
+} PxPal;
+
+typedef struct {
+ unsigned int w, h;
+ PxCol *data;
+} PxSpr;
+
+typedef struct {
+ int _;
+ const PxSpr *spr;
+ int x;
+ int y;
+ int ix;
+ int iy;
+ int w;
+ int h;
+ bool flip_x;
+ bool flip_y;
+} PxSprArgs;
+
+typedef struct {
+ int _;
+ int c0;
+ int c1;
+} PxPalArgs;
+typedef PxPalArgs PxSpalArgs;
+
+typedef struct {
+ int _;
+ int col;
+ bool t;
+} PxPaltArgs;
+
+extern PxCol pxbuf[PX_WIDTH * PX_HEIGHT];
+extern PxPal pxpal[256];
+
+/* Return -1 on error. Should be called after TZR_Init. */
+int pxInit(void);
+
+/* Should be called before TZR_Quit. */
+void pxDeinit(void);
+
+/* Should be called between TZR_DrawBegin and TZR_DrawEnd. */
+void pxFlip(void);
+
+/*** DRAW ***/
+/* Fill the clipping region with color 'c'. */
+void pxCls(PxCol c);
+
+/* Set the clipping region. */
+void pxClip(int x, int y, int w, int h);
+
+/* Reset the clipping region. */
+void pxClipReset(void);
+
+/* Put a pixel. */
+void pxPset(int x, int y, PxCol c);
+
+/* Get a pixel. */
+PxCol pxPget(int x, int y);
+
+/* Draw empty rectangle. */
+void pxRect(int x0, int y0, int x1, int y1, PxCol col);
+
+/* Draw filled rectangle. */
+void pxRectfill(int x0, int y0, int x1, int y1, PxCol col);
+
+/* Draw sprite. */
+#define pxSpr(...) _pxSpr(&(const PxSprArgs){ \
+ .x=0, .y=0, .ix=0, .iy=0, .w=-1, .h=-1, .flip_x=false, .flip_x=false, \
+ ._=0, __VA_ARGS__})
+void _pxSpr(const PxSprArgs *args);
+
+/* Bind palette index to new color. */
+#define pxPal(...) _pxPal(&(const PxPalArgs){ \
+ .c0=-1, .c1=-1, ._=0, __VA_ARGS__})
+void _pxPal(const PxPalArgs *args);
+
+/* Set screen palette. */
+#define pxSpal(...) _pxSpal(&(const PxSpalArgs){ \
+ .c0=-1, .c1=-1, ._=0, __VA_ARGS__})
+void _pxSpal(const PxSpalArgs *args);
+
+/* Set palette tranparency. */
+#define pxPalt(...) _pxPalt(&(const PxPaltArgs){ \
+ .col=-1, .t=true, ._=0, __VA_ARGS__})
+void _pxPalt(const PxPaltArgs *args);
+
+/*** MATH ***/
+int pxMin(int a, int b);
+int pxMax(int a, int b);
+int pxClamp(int min, int max, int v);
+
+/*
+** Copyright (c) 2023 kdx
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+*/
diff --git a/src/tiled2c.h b/src/tiled2c.h
new file mode 100644
index 0000000..f57c83a
--- /dev/null
+++ b/src/tiled2c.h
@@ -0,0 +1,61 @@
+#pragma once
+
+typedef struct {
+ double duration;
+ double tileid;
+} Tiled2cFrame;
+
+typedef struct {
+ unsigned int id;
+ const char *type;
+ double probability;
+ const int numframes;
+ const Tiled2cFrame *frames;
+} Tiled2cTile;
+
+typedef struct {
+ const char *name;
+ const char *path;
+ unsigned int imagewidth;
+ unsigned int imageheight;
+ unsigned int tilewidth;
+ unsigned int tileheight;
+ unsigned int margin;
+ unsigned int columns;
+ unsigned int tilecount;
+ unsigned int numtiles;
+ const Tiled2cTile *tiles;
+} Tiled2cSet;
+
+typedef struct {
+ const char *name;
+ double opacity;
+ unsigned int visible;
+ double parallaxx;
+ double parallaxy;
+ const unsigned int *data;
+} Tiled2cLayer;
+
+typedef struct {
+ const char *name;
+ const char *type;
+ unsigned int id;
+ double x;
+ double y;
+ double width;
+ double height;
+ double rotation;
+ unsigned int visible;
+} Tiled2cObject;
+
+typedef struct {
+ const char *path;
+ unsigned int width;
+ unsigned int height;
+ unsigned int tilewidth;
+ unsigned int tileheight;
+ unsigned int numlayers;
+ const Tiled2cLayer *layers;
+ const Tiled2cObject *objects;
+ unsigned int numobjects;
+} Tiled2cMap;
diff --git a/src/tileset.c b/src/tileset.c
new file mode 100644
index 0000000..b4dc7c6
--- /dev/null
+++ b/src/tileset.c
@@ -0,0 +1,95 @@
+#include "cfg.h"
+#include "tileset.h"
+#include "TZR.h"
+#include "map.h"
+#include "tiled2c.h"
+#include <assert.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <strings.h>
+
+#define MAX_TILE_ID 8192
+
+extern const Tiled2cSet embed_tset_tsj;
+
+static unsigned int tile_types[MAX_TILE_ID] = {};
+static unsigned int tile_visuals[MAX_TILE_ID] = {};
+static unsigned int tile_numframes[MAX_TILE_ID] = {};
+static const Tiled2cFrame *tile_frames[MAX_TILE_ID] = {};
+static unsigned int tile_frame_id[MAX_TILE_ID] = {};
+static long tile_next_frame[MAX_TILE_ID] = {};
+static unsigned long last_tick = 0;
+
+__attribute__((constructor)) static void
+match_tile_types(void)
+{
+ const Tiled2cSet *tset = &embed_tset_tsj;
+ assert(MAX_TILE_ID > tset->tilecount);
+ for (unsigned int i = 0; i < tset->numtiles; i++) {
+ const Tiled2cTile *const tile = &tset->tiles[i];
+ if (tile->type == NULL)
+ ;
+ else if (strcasecmp(tile->type, "solid") == 0)
+ tile_types[tile->id] = TILE_SOLID;
+ else if (strcasecmp(tile->type, "spike") == 0)
+ tile_types[tile->id] = TILE_SPIKE;
+ else if (strcasecmp(tile->type, "smolspike") == 0)
+ tile_types[tile->id] = TILE_SMOLSPIKE;
+ else
+ fprintf(stderr, "unknown tile type '%s'\n", tile->type);
+ if (tile->numframes) {
+ tile_numframes[tile->id] = tile->numframes;
+ tile_frames[tile->id] = tile->frames;
+ }
+ }
+ for (int i = 0; i < MAX_TILE_ID; i++) {
+ tile_frame_id[i] = 0;
+ if (tile_numframes[i]) {
+ tile_visuals[i] = tile_frames[i]->tileid;
+ tile_next_frame[i] = tile_frames[i]->duration *
+ TARGET_FPS / 1000;
+ } else
+ tile_visuals[i] = i;
+ }
+}
+
+unsigned int
+tile_type(unsigned int tile)
+{
+ tile &= ~TILED_TRANSFORMS;
+ if (tile == 0)
+ return TILE_NONE;
+ return tile_types[(tile - 1) & ~TILED_TRANSFORMS];
+}
+
+void
+tileset_update(void)
+{
+ const unsigned long tick = TZR_GetTick();
+ const unsigned long elapsed = tick - last_tick;
+ last_tick = tick;
+
+ for (int i = 0; i < MAX_TILE_ID; i++) {
+ if (!tile_numframes[i])
+ continue;
+ tile_next_frame[i] -= elapsed;
+ while (tile_next_frame[i] <= 0) {
+ if (tile_frame_id[i] == tile_numframes[i] - 1)
+ tile_frame_id[i] = 0;
+ else
+ tile_frame_id[i] += 1;
+ const unsigned int frame_id = tile_frame_id[i];
+ tile_next_frame[i] = tile_frames[i][frame_id]
+ .duration * TARGET_FPS / 1000;
+ tile_visuals[i] = tile_frames[i][frame_id].tileid;
+ }
+ }
+}
+
+unsigned int
+tile_visual(unsigned int tile)
+{
+ if (tile == 0)
+ return 0;
+ return tile_visuals[(tile - 1) & ~TILED_TRANSFORMS];
+}
diff --git a/src/tileset.h b/src/tileset.h
new file mode 100644
index 0000000..6684874
--- /dev/null
+++ b/src/tileset.h
@@ -0,0 +1,12 @@
+#pragma once
+
+enum {
+ TILE_NONE,
+ TILE_SOLID,
+ TILE_SPIKE,
+ TILE_SMOLSPIKE,
+};
+
+unsigned int tile_type(unsigned int tile);
+void tileset_update(void);
+unsigned int tile_visual(unsigned int tile);