diff --git a/.gitignore b/.gitignore index 7176f4b..fd7433a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +.ccls-cache/ **.DS_Store diff --git a/pxl8.sh b/pxl8.sh index b05e31e..00afe9e 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -83,16 +83,12 @@ print_usage() { echo " run Build and run pxl8 [script.fnl|script.lua]" echo " clean Remove build artifacts" echo " update Download/update all dependencies" - echo " vendor Build specific dependencies from source" + echo " vendor Fetch source for dependencies (ex. sdl3)" echo " help Show this help message" echo echo -e "${BOLD}OPTIONS:${NC}" echo " --all Remove all artifacts including dependencies when cleaning" echo " --release Build/run in release mode" - echo - echo -e "${BOLD}VENDOR OPTIONS:${NC}" - echo " --sdl Build SDL3 from source" - echo " --all Build all vendorable dependencies" } COMMAND="$1" @@ -342,8 +338,22 @@ case "$COMMAND" in COMPILE_FLAGS="$CFLAGS $INCLUDES" EXECUTABLE="$BINDIR/pxl8" + LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/miniz/miniz.c" - SRC_SOURCE_FILES="src/pxl8.c src/pxl8_gfx.c src/pxl8_ase.c src/pxl8_font.c src/pxl8_io.c src/pxl8_lua.c src/pxl8_vfx.c src/pxl8_blit.c" + + SRC_SOURCE_FILES=" + src/pxl8.c + src/pxl8_ase.c + src/pxl8_blit.c + src/pxl8_font.c + src/pxl8_gfx.c + src/pxl8_io.c + src/pxl8_lua.c + src/pxl8_tilemap.c + src/pxl8_tilesheet.c + src/pxl8_vfx.c + " + LUAJIT_LIB="lib/luajit/src/libluajit.a" OBJECT_DIR="$BUILDDIR/obj" mkdir -p "$OBJECT_DIR" @@ -439,22 +449,7 @@ case "$COMMAND" in ;; vendor) - for arg in "$@"; do - case $arg in - --sdl) - vendor_sdl - ;; - --all) - vendor_sdl - ;; - *) - ;; - esac - done - - if [[ -z "$1" ]]; then - vendor_sdl - fi + vendor_sdl ;; help|--help|-h|"") diff --git a/res/tiles/tilesheet-dungeon.ase b/res/tiles/tilesheet-dungeon.ase new file mode 100644 index 0000000..573598a Binary files /dev/null and b/res/tiles/tilesheet-dungeon.ase differ diff --git a/res/tiles/tilesheet-world.ase b/res/tiles/tilesheet-world.ase new file mode 100644 index 0000000..e28fab2 Binary files /dev/null and b/res/tiles/tilesheet-world.ase differ diff --git a/src/fnl/demo.fnl b/src/fnl/demo.fnl index 3418d26..8e97781 100644 --- a/src/fnl/demo.fnl +++ b/src/fnl/demo.fnl @@ -1,23 +1,18 @@ (local pxl8 (require :pxl8)) -(var frame 0) (var pxl8-sprite-id nil) -(var screen nil) -(var time 0) (global init (fn [] (pxl8.load_palette "./res/palettes/gruvbox.ase") (set pxl8-sprite-id (pxl8.load_sprite "./res/sprites/pxl8.ase")) - (set screen (pxl8.get_screen)) (when (not pxl8-sprite-id) (pxl8.error "Failed to load pxl8 sprite")))) -(global update (fn [dt])) +(global update (fn [_dt])) (global draw (fn [] (pxl8.clr 1) (local cols 5) - (local rows 1024) (local sprite-w 128) (local sprite-h 64) diff --git a/src/fnl/prydain.fnl b/src/fnl/prydain.fnl new file mode 100644 index 0000000..2d27204 --- /dev/null +++ b/src/fnl/prydain.fnl @@ -0,0 +1,226 @@ +(local pxl8 (require :pxl8)) + +(local TILE_SIZE 16) +(local SCREEN_WIDTH 320) +(local SCREEN_HEIGHT 240) + +(var game-state { + :tilesheet nil + :tilemap nil + :camera {:x 0 :y 0} + :player {:x 5 :y 5 + :screen-x 80 :screen-y 80 + :facing :down + :moving false + :move-timer 0} +}) + +(local tile-types { + :empty 0 + :floor 1 + :wall-top 2 + :wall-front 3 + :wall-left 4 + :wall-right 5 + :wall-corner-tl 6 + :wall-corner-tr 7 + :wall-corner-bl 8 + :wall-corner-br 9 + :door 10 + :chest 11 + :stairs-up 12 + :stairs-down 13 +}) + +(fn generate-room [map x y width height] + (for [ry y (+ y height -1)] + (for [rx x (+ x width -1)] + (when (and (< rx 40) (< ry 30)) + (let [is-wall (or (= rx x) + (= rx (+ x width -1)) + (= ry y) + (= ry (+ y height -1)))] + (if is-wall + (tset map ry rx :wall) + (tset map ry rx :floor))))))) + +(fn generate-dungeon [] + (local map {}) + + (for [y 0 29] + (tset map y {}) + (for [x 0 39] + (tset map y x :empty))) + + (local rooms []) + (local room-count (+ 5 (math.random 5))) + + (for [i 1 room-count] + (local room-w (+ 4 (math.random 6))) + (local room-h (+ 4 (math.random 6))) + (local room-x (math.random 0 (- 39 room-w))) + (local room-y (math.random 0 (- 29 room-h))) + + (generate-room map room-x room-y room-w room-h) + (table.insert rooms {:x (+ room-x (math.floor (/ room-w 2))) + :y (+ room-y (math.floor (/ room-h 2))) + :w room-w + :h room-h})) + + (for [i 1 (- (length rooms) 1)] + (let [room1 (. rooms i) + room2 (. rooms (+ i 1)) + start-x room1.x + start-y room1.y + end-x room2.x + end-y room2.y] + + (var cx start-x) + (var cy start-y) + + (while (not= cx end-x) + (set cx (+ cx (if (< cx end-x) 1 -1))) + (when (= (. map cy cx) :empty) + (tset map cy cx :floor))) + + (while (not= cy end-y) + (set cy (+ cy (if (< cy end-y) 1 -1))) + (when (= (. map cy cx) :empty) + (tset map cy cx :floor))))) + + (let [first-room (. rooms 1)] + (set game-state.player.x first-room.x) + (set game-state.player.y first-room.y) + (set game-state.player.screen-x (* first-room.x TILE_SIZE)) + (set game-state.player.screen-y (* first-room.y TILE_SIZE))) + + map) + +(fn map-to-tilemap [map tilemap] + (for [y 0 29] + (for [x 0 39] + (let [tile (. map y x) + above (and (> y 0) (. map (- y 1) x)) + below (and (< y 29) (. map (+ y 1) x)) + left (and (> x 0) (. map y (- x 1))) + right (and (< x 39) (. map y (+ x 1)))] + + (if (= tile :floor) + (pxl8.tilemap_set_tile tilemap 0 x y tile-types.floor 0) + + (= tile :wall) + (let [is-top-wall (and below (= below :floor)) + is-corner-tl (and (= above :empty) (= left :empty)) + is-corner-tr (and (= above :empty) (= right :empty)) + is-corner-bl (and (= below :floor) (= left :empty)) + is-corner-br (and (= below :floor) (= right :empty))] + + (if is-corner-tl (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-tl pxl8.TILE_SOLID) + is-corner-tr (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-tr pxl8.TILE_SOLID) + is-corner-bl (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-bl pxl8.TILE_SOLID) + is-corner-br (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-br pxl8.TILE_SOLID) + is-top-wall (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-top pxl8.TILE_SOLID) + (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-front pxl8.TILE_SOLID))) + + :else + (pxl8.tilemap_set_tile tilemap 0 x y 0 0)))))) + +(fn move-player [dx dy] + (let [p game-state.player + new-x (+ p.x dx) + new-y (+ p.y dy)] + + (when (and (>= new-x 0) (< new-x 40) + (>= new-y 0) (< new-y 30) + (not (pxl8.tilemap_is_solid game-state.tilemap + (* new-x TILE_SIZE) + (* new-y TILE_SIZE)))) + (set p.x new-x) + (set p.y new-y) + (set p.screen-x (* new-x TILE_SIZE)) + (set p.screen-y (* new-y TILE_SIZE)) + (set p.moving true) + (set p.move-timer 0.2)))) + +(global init (fn [] + (pxl8.info "Prydain RPG initializing...") + + (pxl8.load_palette "./res/palettes/gruvbox.ase") + + (set game-state.tilesheet (pxl8.tilesheet_new TILE_SIZE)) + (set game-state.tilemap (pxl8.tilemap_new 40 30 TILE_SIZE)) + + (if (and game-state.tilesheet game-state.tilemap) + (do + (let [load-result (pxl8.tilesheet_load game-state.tilesheet "./res/tiles/tilesheet-dungeon.ase")] + (if (= load-result 0) + (pxl8.info "Loaded dungeon tilesheet") + (do + (pxl8.warn "Failed to load dungeon tilesheet, using placeholder tiles") + (pxl8.info (.. "Error code: " load-result))))) + + (pxl8.tilemap_set_tilesheet game-state.tilemap game-state.tilesheet) + (pxl8.info "Created tilemap 40x30 and tilesheet") + + (local dungeon-map (generate-dungeon)) + (map-to-tilemap dungeon-map game-state.tilemap) + + (pxl8.info "Random dungeon generated")) + (pxl8.error "Failed to create tilemap or tilesheet")))) + +(global update (fn [dt] + (let [p game-state.player] + + (when (> p.move-timer 0) + (set p.move-timer (- p.move-timer dt)) + (when (<= p.move-timer 0) + (set p.moving false))) + + (when (not p.moving) + (when (pxl8.key_pressed "w") + (set p.facing :up) + (move-player 0 -1)) + (when (pxl8.key_pressed "s") + (set p.facing :down) + (move-player 0 1)) + (when (pxl8.key_pressed "a") + (set p.facing :left) + (move-player -1 0)) + (when (pxl8.key_pressed "d") + (set p.facing :right) + (move-player 1 0)) + (when (pxl8.key_pressed "r") + (pxl8.info "Regenerating dungeon...") + (local new-map (generate-dungeon)) + (map-to-tilemap new-map game-state.tilemap))) + + (let [target-cam-x (- p.screen-x (/ SCREEN_WIDTH 2)) + target-cam-y (- p.screen-y (/ SCREEN_HEIGHT 2))] + (set game-state.camera.x (math.max 0 (math.min target-cam-x (* (- 40 20) TILE_SIZE)))) + (set game-state.camera.y (math.max 0 (math.min target-cam-y (* (- 30 15) TILE_SIZE))))) + + (when game-state.tilemap + (pxl8.tilemap_set_camera game-state.tilemap + game-state.camera.x + game-state.camera.y))))) + +(global draw (fn [] + (pxl8.clr 0) + + (when game-state.tilemap + (pxl8.tilemap_render game-state.tilemap)) + + (let [p game-state.player + px (- p.screen-x game-state.camera.x) + py (- p.screen-y game-state.camera.y)] + + (pxl8.rect_fill px py TILE_SIZE TILE_SIZE 14) + + (case p.facing + :up (pxl8.rect_fill (+ px 6) py 4 4 15) + :down (pxl8.rect_fill (+ px 6) (+ py 12) 4 4 15) + :left (pxl8.rect_fill px (+ py 6) 4 4 15) + :right (pxl8.rect_fill (+ px 12) (+ py 6) 4 4 15))) + + (pxl8.text "WASD: Move | R: New Dungeon" 5 5 15) + (pxl8.text (.. "Pos: " game-state.player.x "," game-state.player.y) 5 15 15))) diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua index c17a09c..30e422b 100644 --- a/src/lua/pxl8.lua +++ b/src/lua/pxl8.lua @@ -189,4 +189,64 @@ function pxl8.gfx_fade_palette(start, count, amount, target_color) C.pxl8_gfx_fade_palette(gfx, start, count, amount, target_color) end +function pxl8.tilesheet_new(tile_size) + return C.pxl8_tilesheet_new(tile_size or 16) +end + +function pxl8.tilesheet_destroy(tilesheet) + C.pxl8_tilesheet_destroy(tilesheet) +end + +function pxl8.tilesheet_load(tilesheet, filepath) + return C.pxl8_tilesheet_load(tilesheet, filepath, gfx) +end + +function pxl8.tilemap_new(width, height, tile_size) + return C.pxl8_tilemap_new(width, height, tile_size or 16) +end + +function pxl8.tilemap_destroy(tilemap) + C.pxl8_tilemap_destroy(tilemap) +end + +function pxl8.tilemap_set_tilesheet(tilemap, tilesheet) + return C.pxl8_tilemap_set_tilesheet(tilemap, tilesheet) +end + +function pxl8.tilemap_set_tile(tilemap, layer, x, y, tile_id, flags) + C.pxl8_tilemap_set_tile(tilemap, layer or 0, x, y, tile_id or 0, flags or 0) +end + +function pxl8.tilemap_get_tile_id(tilemap, layer, x, y) + return C.pxl8_tilemap_get_tile_id(tilemap, layer or 0, x, y) +end + +function pxl8.tilemap_set_camera(tilemap, x, y) + C.pxl8_tilemap_set_camera(tilemap, x, y) +end + +function pxl8.tilemap_render(tilemap) + C.pxl8_tilemap_render(tilemap, gfx) +end + +function pxl8.tilemap_render_layer(tilemap, layer) + C.pxl8_tilemap_render_layer(tilemap, gfx, layer) +end + +function pxl8.tilemap_is_solid(tilemap, x, y) + return C.pxl8_tilemap_is_solid(tilemap, x, y) +end + +function pxl8.tilemap_check_collision(tilemap, x, y, w, h) + return C.pxl8_tilemap_check_collision(tilemap, x, y, w, h) +end + +pxl8.TILE_FLIP_X = 1 +pxl8.TILE_FLIP_Y = 2 +pxl8.TILE_SOLID = 4 +pxl8.TILE_TRIGGER = 8 + +pxl8.gfx = gfx +pxl8.input = input + return pxl8 diff --git a/src/pxl8_lua.c b/src/pxl8_lua.c index 1ab9814..1c9a3fd 100644 --- a/src/pxl8_lua.c +++ b/src/pxl8_lua.c @@ -46,6 +46,22 @@ static const char* pxl8_ffi_cdefs = "bool pxl8_key_down(const pxl8_input_state* input, i32 key);\n" "bool pxl8_key_pressed(const pxl8_input_state* input, i32 key);\n" "\n" +"typedef struct pxl8_tilesheet pxl8_tilesheet;\n" +"typedef struct pxl8_tilemap pxl8_tilemap;\n" +"pxl8_tilesheet* pxl8_tilesheet_new(u32 tile_size);\n" +"void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet);\n" +"i32 pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx_ctx* gfx);\n" +"pxl8_tilemap* pxl8_tilemap_new(u32 width, u32 height, u32 tile_size);\n" +"void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);\n" +"i32 pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet);\n" +"void pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags);\n" +"u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);\n" +"void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y);\n" +"void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx);\n" +"void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx, u32 layer);\n" +"bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y);\n" +"bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);\n" +"\n" "typedef struct {\n" " float x, y, z;\n" " float vx, vy, vz;\n" diff --git a/src/pxl8_macros.h b/src/pxl8_macros.h index cf70710..55b180f 100644 --- a/src/pxl8_macros.h +++ b/src/pxl8_macros.h @@ -81,3 +81,11 @@ static inline void pxl8_log_timestamp(char* buffer, size_t size) { fprintf(stdout, __VA_ARGS__); \ fprintf(stdout, "\n"); \ } while(0) + +#ifndef pxl8_min +#define pxl8_min(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef pxl8_max +#define pxl8_max(a, b) ((a) > (b) ? (a) : (b)) +#endif diff --git a/src/pxl8_tilemap.c b/src/pxl8_tilemap.c new file mode 100644 index 0000000..2db24a3 --- /dev/null +++ b/src/pxl8_tilemap.c @@ -0,0 +1,215 @@ +#include "pxl8_tilemap.h" +#include "pxl8_tilesheet.h" +#include "pxl8_macros.h" +#include +#include + +pxl8_result pxl8_tilemap_init(pxl8_tilemap* tilemap, u32 width, u32 height, u32 tile_size) { + if (!tilemap) return PXL8_ERROR_NULL_POINTER; + if (width > PXL8_MAX_TILEMAP_WIDTH || height > PXL8_MAX_TILEMAP_HEIGHT) { + return PXL8_ERROR_INVALID_SIZE; + } + + memset(tilemap, 0, sizeof(pxl8_tilemap)); + tilemap->width = width; + tilemap->height = height; + tilemap->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE; + tilemap->active_layers = 1; + + for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { + tilemap->layers[i].width = width; + tilemap->layers[i].height = height; + tilemap->layers[i].visible = (i == 0); + tilemap->layers[i].opacity = 255; + + size_t tiles_size = width * height * sizeof(pxl8_tile); + tilemap->layers[i].tiles = calloc(1, tiles_size); + if (!tilemap->layers[i].tiles) { + for (u32 j = 0; j < i; j++) { + free(tilemap->layers[j].tiles); + } + return PXL8_ERROR_OUT_OF_MEMORY; + } + } + + return PXL8_OK; +} + +void pxl8_tilemap_free(pxl8_tilemap* tilemap) { + if (!tilemap) return; + + for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { + if (tilemap->layers[i].tiles) { + free(tilemap->layers[i].tiles); + tilemap->layers[i].tiles = NULL; + } + } +} + +pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet) { + if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER; + + tilemap->tilesheet = tilesheet; + + if (tilesheet->tile_size != tilemap->tile_size) { + pxl8_warn("Tilesheet tile size (%d) differs from tilemap tile size (%d)", + tilesheet->tile_size, tilemap->tile_size); + tilemap->tile_size = tilesheet->tile_size; + } + + return PXL8_OK; +} + +pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags) { + if (!tilemap) return PXL8_ERROR_NULL_POINTER; + if (layer >= PXL8_MAX_TILE_LAYERS) return PXL8_ERROR_INVALID_ARGUMENT; + if (x >= tilemap->width || y >= tilemap->height) return PXL8_ERROR_INVALID_COORDINATE; + + pxl8_tilemap_layer* l = &tilemap->layers[layer]; + u32 idx = y * tilemap->width + x; + l->tiles[idx].id = tile_id; + l->tiles[idx].flags = flags; + + if (layer >= tilemap->active_layers) { + tilemap->active_layers = layer + 1; + l->visible = true; + } + + return PXL8_OK; +} + +pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) { + pxl8_tile empty_tile = {0, 0, 0}; + + if (!tilemap || layer >= PXL8_MAX_TILE_LAYERS) return empty_tile; + if (x >= tilemap->width || y >= tilemap->height) return empty_tile; + + return tilemap->layers[layer].tiles[y * tilemap->width + x]; +} + +void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y) { + if (!tilemap) return; + tilemap->camera_x = x; + tilemap->camera_y = y; +} + +void pxl8_tilemap_get_view(const pxl8_tilemap* tilemap, const pxl8_gfx_ctx* gfx, pxl8_tilemap_view* view) { + if (!tilemap || !gfx || !view) return; + + view->x = -tilemap->camera_x; + view->y = -tilemap->camera_y; + view->width = gfx->framebuffer_width; + view->height = gfx->framebuffer_height; + + view->tile_start_x = pxl8_max(0, tilemap->camera_x / (i32)tilemap->tile_size); + view->tile_start_y = pxl8_max(0, tilemap->camera_y / (i32)tilemap->tile_size); + view->tile_end_x = pxl8_min((i32)tilemap->width, + (tilemap->camera_x + view->width) / (i32)tilemap->tile_size + 1); + view->tile_end_y = pxl8_min((i32)tilemap->height, + (tilemap->camera_y + view->height) / (i32)tilemap->tile_size + 1); +} + +void pxl8_tilemap_render_tile(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx, u16 tile_id, i32 x, i32 y, u8 flags) { + if (!tilemap || !gfx || !tilemap->tilesheet) return; + pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile_id, x, y, flags); +} + +void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx, u32 layer) { + if (!tilemap || !gfx || layer >= tilemap->active_layers) return; + if (!tilemap->tilesheet) { + pxl8_warn("No tilesheet set for tilemap"); + return; + } + + const pxl8_tilemap_layer* l = &tilemap->layers[layer]; + if (!l->visible) return; + + pxl8_tilemap_view view; + pxl8_tilemap_get_view(tilemap, gfx, &view); + + for (i32 ty = view.tile_start_y; ty < view.tile_end_y; ty++) { + for (i32 tx = view.tile_start_x; tx < view.tile_end_x; tx++) { + pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty); + if (tile.id == 0) continue; + + i32 screen_x = tx * tilemap->tile_size - tilemap->camera_x; + i32 screen_y = ty * tilemap->tile_size - tilemap->camera_y; + + pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile.id, screen_x, screen_y, tile.flags); + } + } +} + +void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx) { + if (!tilemap || !gfx) return; + + for (u32 layer = 0; layer < tilemap->active_layers; layer++) { + pxl8_tilemap_render_layer(tilemap, gfx, layer); + } +} + +bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y) { + if (!tilemap) return true; + + u32 tile_x = x / tilemap->tile_size; + u32 tile_y = y / tilemap->tile_size; + + for (u32 layer = 0; layer < tilemap->active_layers; layer++) { + pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tile_x, tile_y); + if (tile.flags & PXL8_TILE_SOLID) return true; + } + + return false; +} + +bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h) { + if (!tilemap) return true; + + i32 left = x / tilemap->tile_size; + i32 top = y / tilemap->tile_size; + i32 right = (x + w - 1) / tilemap->tile_size; + i32 bottom = (y + h - 1) / tilemap->tile_size; + + for (i32 ty = top; ty <= bottom; ty++) { + for (i32 tx = left; tx <= right; tx++) { + if (tx < 0 || tx >= (i32)tilemap->width || + ty < 0 || ty >= (i32)tilemap->height) return true; + + for (u32 layer = 0; layer < tilemap->active_layers; layer++) { + pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty); + if (tile.flags & PXL8_TILE_SOLID) return true; + } + } + } + + return false; +} + +pxl8_tilemap* pxl8_tilemap_new(u32 width, u32 height, u32 tile_size) { + pxl8_tilemap* tilemap = calloc(1, sizeof(pxl8_tilemap)); + if (!tilemap) { + pxl8_error("Failed to allocate tilemap"); + return NULL; + } + + pxl8_result result = pxl8_tilemap_init(tilemap, width, height, tile_size); + if (result != PXL8_OK) { + pxl8_error("Failed to initialize tilemap"); + free(tilemap); + return NULL; + } + + return tilemap; +} + +void pxl8_tilemap_destroy(pxl8_tilemap* tilemap) { + if (!tilemap) return; + pxl8_tilemap_free(tilemap); + free(tilemap); +} + +u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) { + if (!tilemap) return 0; + pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, x, y); + return tile.id; +} \ No newline at end of file diff --git a/src/pxl8_tilemap.h b/src/pxl8_tilemap.h new file mode 100644 index 0000000..764a62a --- /dev/null +++ b/src/pxl8_tilemap.h @@ -0,0 +1,80 @@ +#pragma once + +#include "pxl8_types.h" +#include "pxl8_gfx.h" +#include "pxl8_tilesheet.h" + +#define PXL8_TILE_SIZE 16 +#define PXL8_MAX_TILEMAP_WIDTH 256 +#define PXL8_MAX_TILEMAP_HEIGHT 256 +#define PXL8_MAX_TILE_LAYERS 4 + +typedef enum pxl8_tile_flags { + PXL8_TILE_FLIP_X = 1 << 0, + PXL8_TILE_FLIP_Y = 1 << 1, + PXL8_TILE_SOLID = 1 << 2, + PXL8_TILE_TRIGGER = 1 << 3, +} pxl8_tile_flags; + +typedef struct pxl8_tile { + u16 id; + u8 flags; + u8 palette_offset; +} pxl8_tile; + +typedef struct pxl8_tilemap_layer { + pxl8_tile* tiles; + u32 width; + u32 height; + bool visible; + u8 opacity; +} pxl8_tilemap_layer; + +typedef struct pxl8_tilemap { + pxl8_tilemap_layer layers[PXL8_MAX_TILE_LAYERS]; + u32 width; + u32 height; + u32 tile_size; + u32 active_layers; + + pxl8_tilesheet* tilesheet; + + i32 camera_x; + i32 camera_y; +} pxl8_tilemap; + +typedef struct pxl8_tilemap_view { + i32 x, y; + i32 width, height; + i32 tile_start_x, tile_start_y; + i32 tile_end_x, tile_end_y; +} pxl8_tilemap_view; + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_result pxl8_tilemap_init(pxl8_tilemap* tilemap, u32 width, u32 height, u32 tile_size); +pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet); +void pxl8_tilemap_free(pxl8_tilemap* tilemap); + +pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags); +pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y); + +void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y); +void pxl8_tilemap_get_view(const pxl8_tilemap* tilemap, const pxl8_gfx_ctx* gfx, pxl8_tilemap_view* view); + +void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx); +void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx, u32 layer); +void pxl8_tilemap_render_tile(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx, u16 tile_id, i32 x, i32 y, u8 flags); + +bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y); +bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h); + +pxl8_tilemap* pxl8_tilemap_new(u32 width, u32 height, u32 tile_size); +void pxl8_tilemap_destroy(pxl8_tilemap* tilemap); +u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/pxl8_tilesheet.c b/src/pxl8_tilesheet.c new file mode 100644 index 0000000..f7865a4 --- /dev/null +++ b/src/pxl8_tilesheet.c @@ -0,0 +1,182 @@ +#include "pxl8_tilesheet.h" +#include "pxl8_tilemap.h" +#include "pxl8_ase.h" +#include "pxl8_macros.h" +#include +#include + +pxl8_result pxl8_tilesheet_init(pxl8_tilesheet* tilesheet, u32 tile_size) { + if (!tilesheet) return PXL8_ERROR_NULL_POINTER; + + memset(tilesheet, 0, sizeof(pxl8_tilesheet)); + tilesheet->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE; + + return PXL8_OK; +} + +void pxl8_tilesheet_free(pxl8_tilesheet* tilesheet) { + if (!tilesheet) return; + + if (tilesheet->data) { + free(tilesheet->data); + tilesheet->data = NULL; + } + + if (tilesheet->tile_valid) { + free(tilesheet->tile_valid); + tilesheet->tile_valid = NULL; + } +} + +pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx_ctx* gfx) { + if (!tilesheet || !filepath || !gfx) return PXL8_ERROR_NULL_POINTER; + + pxl8_ase_file ase_file; + pxl8_result result = pxl8_ase_load(filepath, &ase_file); + if (result != PXL8_OK) { + pxl8_error("Failed to load tilesheet: %s", filepath); + return result; + } + + if (tilesheet->data) { + free(tilesheet->data); + } + + u32 width = ase_file.header.width; + u32 height = ase_file.header.height; + + if (ase_file.header.grid_width > 0 && ase_file.header.grid_height > 0) { + tilesheet->tile_size = ase_file.header.grid_width; + pxl8_info("Using Aseprite grid size: %dx%d", + ase_file.header.grid_width, ase_file.header.grid_height); + } + + tilesheet->width = width; + tilesheet->height = height; + tilesheet->tiles_per_row = width / tilesheet->tile_size; + tilesheet->total_tiles = (width / tilesheet->tile_size) * (height / tilesheet->tile_size); + tilesheet->color_mode = gfx->color_mode; + + size_t data_size = width * height; + if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + data_size *= 4; + } + + tilesheet->data = malloc(data_size); + if (!tilesheet->data) { + pxl8_ase_free(&ase_file); + return PXL8_ERROR_OUT_OF_MEMORY; + } + + if (ase_file.frame_count > 0 && ase_file.frames[0].pixels) { + memcpy(tilesheet->data, ase_file.frames[0].pixels, data_size); + } + + tilesheet->tile_valid = calloc(tilesheet->total_tiles + 1, sizeof(bool)); + if (!tilesheet->tile_valid) { + free(tilesheet->data); + tilesheet->data = NULL; + pxl8_ase_free(&ase_file); + return PXL8_ERROR_OUT_OF_MEMORY; + } + + u32 valid_tiles = 0; + bool is_hicolor = (tilesheet->color_mode == PXL8_COLOR_MODE_HICOLOR); + + for (u32 tile_id = 1; tile_id <= tilesheet->total_tiles; tile_id++) { + u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size; + u32 tile_y = ((tile_id - 1) / tilesheet->tiles_per_row) * tilesheet->tile_size; + + bool has_content = false; + for (u32 py = 0; py < tilesheet->tile_size && !has_content; py++) { + for (u32 px = 0; px < tilesheet->tile_size; px++) { + if (is_hicolor) { + u32 idx = ((tile_y + py) * width + (tile_x + px)) * 4; + u8 alpha = tilesheet->data[idx + 3]; + if (alpha > 0) { + has_content = true; + break; + } + } else { + u32 idx = (tile_y + py) * width + (tile_x + px); + if (tilesheet->data[idx] != 0) { + has_content = true; + break; + } + } + } + } + if (has_content) { + tilesheet->tile_valid[tile_id] = true; + valid_tiles++; + } + } + + pxl8_info("Loaded tilesheet %s: %dx%d, %d valid tiles out of %d slots (%dx%d each)", + filepath, width, height, valid_tiles, tilesheet->total_tiles, + tilesheet->tile_size, tilesheet->tile_size); + + pxl8_ase_free(&ase_file); + return PXL8_OK; +} + +void pxl8_tilesheet_render_tile(const pxl8_tilesheet* tilesheet, pxl8_gfx_ctx* gfx, + u16 tile_id, i32 x, i32 y, u8 flags) { + if (!tilesheet || !gfx || !tilesheet->data) return; + if (tile_id == 0 || tile_id > tilesheet->total_tiles) return; + if (tilesheet->tile_valid && !tilesheet->tile_valid[tile_id]) return; + + u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size; + u32 tile_y = ((tile_id - 1) / tilesheet->tiles_per_row) * tilesheet->tile_size; + + for (u32 py = 0; py < tilesheet->tile_size; py++) { + for (u32 px = 0; px < tilesheet->tile_size; px++) { + u32 src_x = tile_x + px; + u32 src_y = tile_y + py; + + if (flags & PXL8_TILE_FLIP_X) src_x = tile_x + (tilesheet->tile_size - 1 - px); + if (flags & PXL8_TILE_FLIP_Y) src_y = tile_y + (tilesheet->tile_size - 1 - py); + + u32 src_idx = src_y * tilesheet->width + src_x; + u8 color_idx = tilesheet->data[src_idx]; + + if (color_idx != 0) { + i32 screen_x = x + px; + i32 screen_y = y + py; + + if (screen_x >= 0 && screen_x < gfx->framebuffer_width && + screen_y >= 0 && screen_y < gfx->framebuffer_height) { + pxl8_pixel(gfx, screen_x, screen_y, color_idx); + } + } + } + } +} + +bool pxl8_tilesheet_is_tile_valid(const pxl8_tilesheet* tilesheet, u16 tile_id) { + if (!tilesheet || tile_id == 0 || tile_id > tilesheet->total_tiles) return false; + return tilesheet->tile_valid ? tilesheet->tile_valid[tile_id] : true; +} + +pxl8_tilesheet* pxl8_tilesheet_new(u32 tile_size) { + pxl8_tilesheet* tilesheet = calloc(1, sizeof(pxl8_tilesheet)); + if (!tilesheet) { + pxl8_error("Failed to allocate tilesheet"); + return NULL; + } + + pxl8_result result = pxl8_tilesheet_init(tilesheet, tile_size); + if (result != PXL8_OK) { + pxl8_error("Failed to initialize tilesheet"); + free(tilesheet); + return NULL; + } + + return tilesheet; +} + +void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet) { + if (!tilesheet) return; + pxl8_tilesheet_free(tilesheet); + free(tilesheet); +} \ No newline at end of file diff --git a/src/pxl8_tilesheet.h b/src/pxl8_tilesheet.h new file mode 100644 index 0000000..e40fdbf --- /dev/null +++ b/src/pxl8_tilesheet.h @@ -0,0 +1,35 @@ +#pragma once + +#include "pxl8_types.h" +#include "pxl8_gfx.h" + +typedef struct pxl8_tilesheet { + u8* data; + bool* tile_valid; + u32 width; + u32 height; + u32 tile_size; + u32 tiles_per_row; + u32 total_tiles; + pxl8_color_mode color_mode; +} pxl8_tilesheet; + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_result pxl8_tilesheet_init(pxl8_tilesheet* tilesheet, u32 tile_size); +pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx_ctx* gfx); +void pxl8_tilesheet_free(pxl8_tilesheet* tilesheet); + +void pxl8_tilesheet_render_tile(const pxl8_tilesheet* tilesheet, pxl8_gfx_ctx* gfx, + u16 tile_id, i32 x, i32 y, u8 flags); + +bool pxl8_tilesheet_is_tile_valid(const pxl8_tilesheet* tilesheet, u16 tile_id); + +pxl8_tilesheet* pxl8_tilesheet_new(u32 tile_size); +void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet); + +#ifdef __cplusplus +} +#endif \ No newline at end of file