fangflecked

90s style ARPG
git clone git://moonbender.net/fangflecked
Log | Files | Refs | README

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:
A.gitignore | 6++++++
ACMakeLists.txt | 22++++++++++++++++++++++
AREADME | 24++++++++++++++++++++++++
Aassets/d1warriorstand.bmp | 0
Aassets/d1warriorwalk.bmp | 0
Aassets/ph_tile.bmp | 0
Aassets/ph_tile2.bmp | 0
Aassets/ph_tile3.bmp | 0
Afangflecked.c | 454+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aff.html | 27+++++++++++++++++++++++++++
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> +