commit 09d674bd9907c19729d8ef0e07e5a0bb60c869f3
Author: Calliope <me@calliope.sh>
Date: Tue, 16 Sep 2025 22:42:43 -0500
these forests eke, made wretched by our music
Diffstat:
10 files changed, 533 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,6 @@
+build
+vendored
+*.o
+*core*
+Makefile
+~*
diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -0,0 +1,22 @@
+cmake_minimum_required(VERSION 3.16)
+project(fangflecked)
+
+# set the output directory for built objects.
+# This makes sure that the dynamic library goes into the build directory automatically.
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIGURATION>")
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIGURATION>")
+
+# This assumes the SDL source is available in vendored/SDL
+add_subdirectory(vendored/SDL EXCLUDE_FROM_ALL)
+
+# Create your game executable target as usual
+add_executable(ff fangflecked.c)
+
+# Embed assets if compiling for web
+if(EMSCRIPTEN)
+ set_target_properties(ff PROPERTIES LINK_FLAGS "--embed-file ../assets/d1warriorstand.bmp@assets/d1warriorstand.bmp --embed-file ../assets/d1warriorwalk.bmp@assets/d1warriorwalk.bmp --embed-file ../assets/ph_tile.bmp@assets/ph_tile.bmp --embed-file ../assets/ph_tile2.bmp@assets/ph_tile2.bmp --embed-file ../assets/ph_tile3.bmp@assets/ph_tile3.bmp")
+endif()
+
+# Link to the actual SDL3 library.
+target_link_libraries(ff PRIVATE SDL3::SDL3)
+target_compile_options(ff PRIVATE -std=c89 -Wall)
diff --git a/README b/README
@@ -0,0 +1,24 @@
+First you need a copy of the SDL source:
+
+git clone https://github.com/libsdl-org/SDL.git vendored/SDL
+
+You also need cmake installed (and emscripten if you're building for web). On FreeBSD:
+
+pkg install cmake emscripten
+
+Local build:
+
+cmake -S . -B build
+cmake --build build
+
+You'll want to run the binary from the base directory so asset paths are correct:
+
+build/ff
+
+Web:
+
+emcmake cmake -S . -B build
+cd build
+emmake make
+
+Have fun <3
diff --git a/assets/d1warriorstand.bmp b/assets/d1warriorstand.bmp
Binary files differ.
diff --git a/assets/d1warriorwalk.bmp b/assets/d1warriorwalk.bmp
Binary files differ.
diff --git a/assets/ph_tile.bmp b/assets/ph_tile.bmp
Binary files differ.
diff --git a/assets/ph_tile2.bmp b/assets/ph_tile2.bmp
Binary files differ.
diff --git a/assets/ph_tile3.bmp b/assets/ph_tile3.bmp
Binary files differ.
diff --git a/fangflecked.c b/fangflecked.c
@@ -0,0 +1,454 @@
+#define SDL_MAIN_USE_CALLBACKS 1 /* use the callbacks instead of main() */
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+#include <stdio.h>
+
+#define LEVEL_MAX_WIDTH 8
+#define LEVEL_MAX_HEIGHT 8
+#define SCREEN_WIDTH 1024
+#define SCREEN_HEIGHT 768
+#define TILE_PIXEL_HEIGHT 64
+#define TILE_WIDTH (TILE_PIXEL_HEIGHT * 2 * scale)
+#define TILE_HEIGHT (TILE_PIXEL_HEIGHT * scale)
+
+#define MAP_RENDER_OFFSET_X 0
+#define MAP_RENDER_OFFSET_Y SCREEN_HEIGHT / 2
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+
+float scale = 1;
+
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+
+/* assets */
+SDL_Texture *texture_tile, *texture_tile2, *texture_tile3;
+SDL_Texture *texture_warrior_stand, *texture_warrior_walk;
+
+typedef struct {
+ u32 size;
+ u32 cur;
+ u32 tiles[LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT];
+} NavPath;
+
+/* player world info */
+typedef struct {
+ u32 pos;
+ u32 pos_next;
+ u8 angle;
+ u8 state;
+ u8 frame;
+
+ /* timers */
+ u32 frame_next;
+ u32 state_next;
+
+ /* pathing */
+ NavPath path;
+} PC;
+
+PC player;
+
+enum {
+ PLAYER_STATE_STANDING = 0,
+ PLAYER_STATE_WALKING
+};
+
+enum {
+ ANGLE_DOWN = 0,
+ ANGLE_DOWN_LEFT,
+ ANGLE_LEFT,
+ ANGLE_UP_LEFT,
+ ANGLE_UP,
+ ANGLE_UP_RIGHT,
+ ANGLE_RIGHT,
+ ANGLE_DOWN_RIGHT
+};
+
+/* level data */
+u8 level_tiles[LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT];
+
+typedef struct {
+ float x, y;
+} vecf;
+
+/*
+ * functions for drawing and interacting with the level
+ * ====================================================
+ * TODO: give these better names
+ */
+
+/* lerp between two points */
+vecf vecf_lerp(vecf p1, vecf p2, float t) {
+ vecf result;
+
+ result.x = p1.x * (1.0 - t) + p2.x * t;
+ result.y = p1.y * (1.0 - t) + p2.y * t;
+ return result;
+}
+
+vecf pos_world(u32 tile) {
+ vecf result;
+
+ result.x = SDL_floor(tile % LEVEL_MAX_WIDTH);
+ result.y = SDL_floor(tile / LEVEL_MAX_WIDTH);
+ return result;
+}
+
+/* Return the screen position of the passed tile */
+vecf iso_coords(u32 tile) {
+ vecf world;
+ vecf result;
+ u32 offs_x, offs_y;
+
+ offs_x = TILE_WIDTH / 2;
+ offs_y = TILE_HEIGHT / 2;
+
+ world = pos_world(tile);
+ result.x = world.x * offs_x + world.y * offs_x;
+ result.y = world.y * offs_y - world.x * offs_y;
+
+ result.x += MAP_RENDER_OFFSET_X;
+ result.y += MAP_RENDER_OFFSET_Y;
+
+ return result;
+}
+
+u8 path_line(u32 t1, u32 t2, NavPath *line) {
+ u32 dx, dy;
+ u32 i;
+ u32 tx, ty;
+ vecf p1, p2;
+ vecf lerp_point;
+
+ if (t1 == t2) { return 1; }
+
+ line->cur = 0;
+ p1 = pos_world(t1);
+ p2 = pos_world(t2);
+
+ dx = SDL_abs(p2.x - p1.x);
+ dy = SDL_abs(p2.y - p1.y);
+ line->size = dx > dy ? dx : dy;
+ for (i = 1; i <= line->size; i++) {
+ lerp_point = vecf_lerp(p1, p2, (float) i / (float) line->size);
+ tx = SDL_round(lerp_point.x);
+ ty = SDL_round(lerp_point.y);
+ line->tiles[i-1] = ty * LEVEL_MAX_WIDTH + tx;
+ }
+ return 0;
+}
+
+u8 path_next(NavPath *path, u32 *tile) {
+ if (path->cur >= path->size) { return 1; }
+ *tile = path->tiles[path->cur];
+ path->cur++;
+ return 0;
+}
+
+u8 path_reset(NavPath *path) {
+ path->cur = 0;
+ path->size = 0;
+ return 0;
+}
+
+/* Calculate the tile that the cursor is currently hovering over */
+u8 cursor_tile(float screen_x, float screen_y, u32 *tile) {
+ u32 result;
+ float world_x, world_y, offs_x, offs_y;
+
+ *tile = 0;
+
+ offs_x = TILE_WIDTH / 2;
+ offs_y = TILE_HEIGHT / 2;
+
+ screen_x -= offs_x + MAP_RENDER_OFFSET_X;
+ screen_y -= offs_y + MAP_RENDER_OFFSET_Y;
+
+ world_x = SDL_roundf(screen_x / TILE_WIDTH - screen_y / TILE_HEIGHT);
+ world_y = SDL_roundf(screen_x / TILE_WIDTH + screen_y / TILE_HEIGHT);
+
+ if (world_x < 0 || world_y < 0)
+ return 1;
+
+ if (world_y > LEVEL_MAX_HEIGHT || world_x >= LEVEL_MAX_WIDTH)
+ return 1;
+
+ result = world_y * LEVEL_MAX_WIDTH + world_x;
+ if (result < (LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT)) {
+ *tile = result;
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+void draw_level(void) {
+ u32 i;
+ vecf pos;
+ SDL_FRect dest;
+ SDL_FRect src;
+
+ dest.w = (float) 128 * scale;
+ dest.h = (float) 64 * scale;
+ src.w = (float) 128;
+ src.h = (float) 64;
+ src.x = 0;
+ src.y = 0;
+
+ for(i = 0; i < LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT; i++) {
+ pos = iso_coords(i);
+ dest.x = pos.x;
+ dest.y = pos.y;
+
+ if (level_tiles[i] == 1) {
+ SDL_RenderTexture(renderer, texture_tile, &src, &dest);
+ } else if (level_tiles[i] == 2) {
+ SDL_RenderTexture(renderer, texture_tile2, &src, &dest);
+ } else if (level_tiles[i] == 3) {
+ SDL_RenderTexture(renderer, texture_tile3, &src, &dest);
+ }
+ }
+}
+
+/*
+ * Player drawing and logic
+ * =====================================================
+ */
+
+/*
+ * check if player can move to the passed tile,
+ * then handle the movement
+ */
+u8 player_move(u32 tile) {
+ u32 target_x, target_y, current_x, current_y;
+
+ if (tile == player.pos) { return 1; }
+ if (player.pos != player.pos_next) { return 1; } /* Don't interrupt if player is already between tiles */
+
+ target_x = tile % LEVEL_MAX_WIDTH;
+ target_y = tile / LEVEL_MAX_WIDTH;
+ current_x = player.pos % LEVEL_MAX_WIDTH;
+ current_y = player.pos / LEVEL_MAX_WIDTH;
+
+ /* axis-aligned movement (relative to the screen) is kitty corner according to the world coordinates */
+ if (target_x - current_x == 1 && target_y - current_y == 1) {
+ player.angle = ANGLE_RIGHT;
+ } else if (target_x - current_x == 1 && target_y - current_y == -1) {
+ player.angle = ANGLE_UP;
+ } else if (target_x - current_x == -1 && target_y - current_y == -1) {
+ player.angle = ANGLE_LEFT;
+ } else if (target_x - current_x == -1 && target_y - current_y == 1) {
+ player.angle = ANGLE_DOWN;
+ /* diagonal (relative to the screen) is along the grid in world coordinates */
+ } else if (target_x - current_x == 1 && target_y - current_y == 0) {
+ player.angle = ANGLE_UP_RIGHT;
+ } else if (target_x - current_x == 0 && target_y - current_y == -1) {
+ player.angle = ANGLE_UP_LEFT;
+ } else if (target_x - current_x == -1 && target_y - current_y == 0) {
+ player.angle = ANGLE_DOWN_LEFT;
+ } else if (target_x - current_x == 0 && target_y - current_y == 1) {
+ player.angle = ANGLE_DOWN_RIGHT;
+ } else {
+ /* tile not adjacent */
+ return 1;
+ }
+
+ player.pos_next = tile;
+ player.state = PLAYER_STATE_WALKING;
+ player.frame = 0;
+ player.state_next = SDL_GetTicks() + 400;
+ player.frame_next = SDL_GetTicks() + 50;
+ return 0;
+}
+
+u8 player_path(u32 tile) {
+ /* adjacent tile, no pathing needed */
+ if (player_move(tile) == 0) { return 0; }
+
+ if (path_line(player.pos_next, tile, &player.path) == 0) {
+ return 0;
+ }
+ return 1;
+}
+
+u8 draw_player() {
+ SDL_FRect dest;
+ SDL_FRect src;
+ vecf offs;
+ u32 tile_next;
+
+ src.y = (float) player.angle * 96;
+ src.x = (float) player.frame * 96;
+ src.w = (float) 96;
+ src.h = (float) 96;
+
+ dest.w = (float) 160 * scale;
+ dest.h = (float) 160 * scale;
+
+ if (SDL_GetTicks() >= player.frame_next) {
+ player.frame++;
+ player.frame_next = SDL_GetTicks() + 50;
+
+ if (player.state == PLAYER_STATE_STANDING) { /* standing anim has 10 frames */
+ if (player.frame > 9) { player.frame = 0; }
+ } else if (player.state == PLAYER_STATE_WALKING) {
+ if (player.frame > 7) { player.frame = 0; }
+ }
+ }
+
+ /*
+ * logic for switching states and moving tiles
+ * mixed in with animation code
+ * TODO: definitely want to untangle this
+ */
+
+ if (player.state == PLAYER_STATE_WALKING) {
+ if (SDL_GetTicks() >= player.state_next) {
+ player.pos = player.pos_next;
+ offs = iso_coords(player.pos);
+
+ /*
+ * check if there's another tile left in the pathing queue
+ * if not, switch state back to standing
+ */
+
+ if (path_next(&player.path, &tile_next) == 0) {
+ player_move(tile_next);
+ } else {
+ player.frame = 0;
+ player.frame_next = SDL_GetTicks() + 50;
+ player.state = PLAYER_STATE_STANDING;
+ path_reset(&player.path);
+ }
+ } else {
+ /* lerp to figure out where to draw the player sprite as they walk between tiles */
+ offs = vecf_lerp(iso_coords(player.pos_next), iso_coords(player.pos), (float) (player.state_next - SDL_GetTicks()) / 400);
+ }
+ } else if (player.state == PLAYER_STATE_STANDING) {
+ if (path_next(&player.path, &tile_next) == 0) {
+ player_move(tile_next);
+ }
+ offs = iso_coords(player.pos);
+ }
+
+ dest.x = offs.x;
+ dest.y = offs.y - dest.h + TILE_HEIGHT;
+
+ if (player.state == PLAYER_STATE_WALKING) {
+ SDL_RenderTextureRotated(renderer, texture_warrior_walk, &src , &dest, 0, NULL, SDL_FLIP_NONE);
+ } else if (player.state == PLAYER_STATE_STANDING) {
+ SDL_RenderTextureRotated(renderer, texture_warrior_stand, &src , &dest, 0, NULL, SDL_FLIP_NONE);
+ }
+ return 0;
+}
+
+/*
+ * SDL Callbacks
+ * ========================================================
+ */
+
+SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
+ SDL_Surface *buffer;
+ u8 i;
+
+ for (i = 0; i < LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT; i++) {
+ /* level_tiles[i] = i % 2 + 1; */
+ level_tiles[i] = 1;
+ }
+
+
+ SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO);
+ if (!SDL_CreateWindowAndRenderer("FANGFLECKED", SCREEN_WIDTH, SCREEN_HEIGHT, 0, &window, &renderer)) {
+ SDL_Log("Couldn't create window and renderer: %s", SDL_GetError());
+ return SDL_APP_FAILURE;
+ }
+
+ /* load graphics from file */
+ buffer = SDL_LoadBMP("assets/ph_tile.bmp");
+ SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
+ texture_tile = SDL_CreateTextureFromSurface(renderer, buffer);
+ buffer = SDL_LoadBMP("assets/ph_tile2.bmp");
+ SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
+ texture_tile2 = SDL_CreateTextureFromSurface(renderer, buffer);
+ buffer = SDL_LoadBMP("assets/ph_tile3.bmp");
+ SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
+ texture_tile3 = SDL_CreateTextureFromSurface(renderer, buffer);
+ buffer = SDL_LoadBMP("assets/d1warriorstand.bmp");
+ SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
+ texture_warrior_stand = SDL_CreateTextureFromSurface(renderer, buffer);
+ buffer = SDL_LoadBMP("assets/d1warriorwalk.bmp");
+ SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
+ texture_warrior_walk = SDL_CreateTextureFromSurface(renderer, buffer);
+ SDL_DestroySurface(buffer);
+
+ player.pos = 17;
+ player.pos_next = 17;
+ player.angle = ANGLE_DOWN;
+ player.frame_next = 0;
+ player.frame = 0;
+ player.state = PLAYER_STATE_STANDING;
+
+ return SDL_APP_CONTINUE;
+}
+
+SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
+ if (event->type == SDL_EVENT_QUIT) {
+ return SDL_APP_SUCCESS;
+ }
+ return SDL_APP_CONTINUE;
+}
+
+SDL_AppResult SDL_AppIterate(void *appstate) {
+ SDL_MouseButtonFlags mouse_state;
+ float mouse_x, mouse_y;
+ u32 hovertile;
+ u32 i;
+
+ /* key_state = SDL_GetKeyboardState(NULL); */
+ for (i = 0; i < LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT; i++) {
+ level_tiles[i] = 1;
+ }
+
+ mouse_state = SDL_GetMouseState(&mouse_x, &mouse_y);
+
+ SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
+ SDL_RenderClear(renderer);
+
+ /*
+ if (key_state[SDL_SCANCODE_UP]) { posy--; }
+ if (key_state[SDL_SCANCODE_LEFT]) { posx--; }
+ if (key_state[SDL_SCANCODE_DOWN]) { posy++; }
+ if (key_state[SDL_SCANCODE_RIGHT]) { posx++; }
+ */
+
+ if (mouse_state & SDL_BUTTON_LEFT) {
+ if (cursor_tile(mouse_x, mouse_y, &hovertile) == 0) {
+ player_path(hovertile);
+ }
+ }
+
+ for (i = 0; i < player.path.size; i++) {
+ level_tiles[player.path.tiles[i]] = 2;
+ }
+ level_tiles[player.pos_next] = 3;
+
+ draw_level();
+ draw_player();
+
+
+ SDL_RenderPresent(renderer);
+ return SDL_APP_CONTINUE;
+}
+
+void SDL_AppQuit(void *appstate, SDL_AppResult result) {
+ SDL_DestroyTexture(texture_tile);
+ SDL_DestroyTexture(texture_tile2);
+ SDL_DestroyTexture(texture_tile3);
+ SDL_DestroyTexture(texture_warrior_stand);
+ SDL_DestroyTexture(texture_warrior_walk);
+}
diff --git a/ff.html b/ff.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+
+<body>
+
+ <!-- Create the canvas that the C++ code will draw into -->
+ <canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
+
+ <!-- Allow the C++ to access the canvas element -->
+ <script type='text/javascript'>
+ var Module = {
+ canvas: (function() { return document.getElementById('canvas'); })()
+ };
+ </script>
+
+ <!-- Add the javascript glue code (index.js) as generated by Emscripten -->
+ <script src="build/ff.js"></script>
+
+</body>
+
+</html>
+