phantasia

Phantasia - 2D SDL3 RPG prototype.
git clone git://git.beep.wimdupont.com/phantasia.git
Log | Files | Refs | README | LICENSE

commit d50f4af83f3a4c083c743eb3e78b9a10b053ac15
parent e66c15d34176c7ef99ff1114cf3cfe7fff38eab6
Author: beep <beep@wimdupont.com>
Date:   Sat, 18 Jul 2026 15:26:24 +0000

Move game definitions into compile-time config

Diffstat:
M.gitignore | 1+
MMakefile | 10++++++++--
MREADME.adoc | 36++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/engine/world.c | 57+++++++++++++++++++++++++++++++--------------------------
Msrc/engine/world.h | 28++++++++++++++++++++++++----
Msrc/game/main.c | 289++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/tools/mapc.c | 36+++++++++++++++++++++++++++++++++---
8 files changed, 395 insertions(+), 185 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,4 +1,5 @@ bin/ obj/ +config.h *.o *.d diff --git a/Makefile b/Makefile @@ -18,6 +18,7 @@ SRC = \ src/game/main.c MAPC_SRC = src/tools/mapc.c +CONFIG = config.h OBJ = $(SRC:src/%.c=$(OBJDIR)/%.o) DEP = $(OBJ:.o=.d) @@ -25,7 +26,7 @@ DEP = $(OBJ:.o=.d) SDL3_CFLAGS = $(shell pkg-config --cflags sdl3 2>/dev/null) SDL3_LDLIBS = $(shell pkg-config --libs sdl3 2>/dev/null) -CPPFLAGS = -Isrc +CPPFLAGS = -I. -Isrc CFLAGS = -std=c99 -Wall -Wextra -Wpedantic -O2 -MMD -MP LDLIBS = -lm @@ -45,7 +46,7 @@ $(BIN): $(OBJ) $(MAPBIN) $(TILESET_BMP) $(HERO_BMP) $(ITEMS_BMP) $(CRITTERS_BMP) @mkdir -p $(dir $@) $(CC) $(OBJ) -o $@ $(LDLIBS) -$(MAPC): $(MAPC_SRC) +$(MAPC): $(MAPC_SRC) $(CONFIG) @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@ @@ -53,6 +54,11 @@ $(OBJDIR)/%.o: src/%.c @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ +$(OBJ): $(CONFIG) + +$(CONFIG): + cp config.def.h $@ + $(MAPDIR)/%.phmap: data/maps/%.txt $(MAPC) @mkdir -p $(dir $@) $(MAPC) $< $@ diff --git a/README.adoc b/README.adoc @@ -10,6 +10,42 @@ * Story should stay optional and discoverable instead of interrupting gameplay. * Keep most systems generic and engine-like so monsters, NPCs, areas, and loot can be expanded without hardcoding one-off cases. +== Principles + +Phantasia's gameplay and implementation may change substantially while it is +being explored. These principles should not: + +* Performance and simplicity come first, in both the game and its code. +* Concentrate on the core gameplay loop and the core code before broadening the + game. Put foundations in place that can be expanded through data and + configuration instead of accumulating special cases. +* The game should be easy to grasp. Like chess, a small set of clear rules can + create depth through their interactions rather than through a large number of + mechanics. +* Prefer the smallest straightforward implementation that does the job. Remove, + reuse, or combine code before adding another abstraction or special case. +* Lines of code and file count are design costs. A feature that adds a + substantial amount of code, or requires another file, needs proportionate + value and careful consideration. +* Consider every change carefully and verify its effects. Preserve existing + behavior deliberately; avoid regression bugs and unintended side effects. +* Values that are expected to vary belong in configuration, not fixed in core + code. Follow the suckless `config.def.h`/`config.h` model: ship useful defaults + while keeping local configuration compile-time, visible, and easy to change. +* Small code must remain clear and correct. Fewer lines are a means to simplicity, + not a reason to hide behavior or make the code cryptic. + +== Configuration + +The first build copies `config.def.h` to `config.h`. Edit `config.h` to change +local compile-time settings; it is ignored by Git and is not overwritten by +normal later builds. `config.def.h` contains the distributed defaults. + +Configuration currently owns the initial world definitions, entity spawns, +item drops, display dimensions, asset paths and color keys, map tile choices, +and controls. Adding content should normally extend these configured arrays +without adding another special case to the core initialization code. + == Build [source,sh] diff --git a/config.def.h b/config.def.h @@ -0,0 +1,123 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define PH_VIEW_W 320 +#define PH_VIEW_H 240 +#define PH_SCALE 3 +#define PH_WINDOW_TITLE "phantasia" +#define PH_MAX_FRAME_TIME 0.05f +#define PH_FRAME_DELAY_MS 16 + +#define PH_ENTITY_BLOCK_RADIUS 10 +#define PH_PICKUP_RADIUS 18.0f +#define PH_NOTICE_SECONDS 1.25f + +#define PH_START_MAP_PATH "obj/maps/ashen-meadow.phmap" +#define PH_RENDER_SMOKE_PATH "obj/render-smoke.bmp" + +#define PH_CLEAR_COLOR 16, 18, 24, 255 +#define PH_ITEM_COLOR 242, 214, 80, 255 +#define PH_MONSTER_COLOR 168, 108, 224, 255 +#define PH_NPC_COLOR 220, 180, 90, 255 +#define PH_NOTICE_COLOR 20, 24, 34, 255 +#define PH_NOTICE_TEXT_COLOR 232, 236, 244, 255 + +enum { + PH_ASSET_NONE, + PH_ASSET_TILESET, + PH_ASSET_HERO, + PH_ASSET_ITEMS, + PH_ASSET_CRITTERS, +}; + +static const struct { + const char *path; + unsigned char key_r, key_g, key_b; +} ph_asset_defs[] = { + [PH_ASSET_TILESET] = { "obj/assets/overworld_tileset_grass.bmp", 0, 0, 0 }, + [PH_ASSET_HERO] = { "obj/assets/hero.bmp", 0, 0, 0 }, + [PH_ASSET_ITEMS] = { "obj/assets/items.bmp", 111, 119, 109 }, + [PH_ASSET_CRITTERS] = { "obj/assets/critters.bmp", 129, 255, 94 }, +}; + +static const PhTileDef ph_tile_defs[] = { + { '.', 0, PH_ASSET_TILESET, 1, 1 }, + { '#', 1, PH_ASSET_TILESET, 1, 5 }, +}; + +enum { + PH_DEF_PLAYER = 1, + PH_DEF_SLIME, +}; + +enum { + PH_ITEM_TALENT_SHARD = 1, +}; + +static const PhEntityDef ph_entity_defs[] = { + { + .id = PH_DEF_PLAYER, + .name = "Wayfarer", + .max_hp = 40, + .move_speed = 90, + .asset_id = PH_ASSET_HERO, + .sprite_tiles = { { 4, 0 }, { 0, 0 }, { 0, 1 } }, + .blocks_movement = 0, + .kind = PH_ENTITY_PLAYER, + }, + { + .id = PH_DEF_SLIME, + .name = "Mire Slime", + .max_hp = 12, + .move_speed = 45, + .asset_id = PH_ASSET_CRITTERS, + .sprite_tiles = { { 12, 0 }, { 12, 0 }, { 12, 0 } }, + .blocks_movement = 1, + .kind = PH_ENTITY_MONSTER, + }, +}; + +static const PhItemDef ph_item_defs[] = { + { + .id = PH_ITEM_TALENT_SHARD, + .name = "Talent Shard", + .value = 30, + .power = 0, + .asset_id = PH_ASSET_ITEMS, + .sprite_tile_x = 2, + .sprite_tile_y = 6, + }, +}; + +static const struct { + int type_id; + PhVec2 pos; +} ph_entity_spawns[] = { + { PH_DEF_PLAYER, { 80.0f, 80.0f } }, + { PH_DEF_SLIME, { 180.0f, 120.0f } }, +}; + +#define PH_PLAYER_SPAWN 0 + +static const struct { + int item_id; + PhVec2 pos; + int amount; +} ph_item_drops[] = { + { PH_ITEM_TALENT_SHARD, { 104.0f, 80.0f }, 1 }, +}; + +#if PH_USE_SDL +#define PH_KEY_LEFT_1 SDL_SCANCODE_LEFT +#define PH_KEY_LEFT_2 SDL_SCANCODE_A +#define PH_KEY_RIGHT_1 SDL_SCANCODE_RIGHT +#define PH_KEY_RIGHT_2 SDL_SCANCODE_D +#define PH_KEY_UP_1 SDL_SCANCODE_UP +#define PH_KEY_UP_2 SDL_SCANCODE_W +#define PH_KEY_DOWN_1 SDL_SCANCODE_DOWN +#define PH_KEY_DOWN_2 SDL_SCANCODE_S +#define PH_KEY_INTERACT_1 SDL_SCANCODE_E +#define PH_KEY_INTERACT_2 SDL_SCANCODE_SPACE +#endif + +#endif diff --git a/src/engine/world.c b/src/engine/world.c @@ -1,4 +1,5 @@ #include "engine/world.h" +#include "config.h" #include <stdint.h> #include <stdio.h> @@ -6,12 +7,7 @@ #include <math.h> #include <string.h> -enum { - PH_MAP_MAGIC = 0x50484d50, - PH_MAP_VERSION = 1, - PH_AREA_NAME_MAX = 64, - PH_ENTITY_BLOCK_RADIUS = 10, -}; +#define LEN(a) (sizeof(a) / sizeof(0[a])) typedef struct { uint32_t magic; @@ -112,7 +108,7 @@ static void ph_world_set_notice(PhWorld *world, const char *text) { snprintf(world->notice, sizeof(world->notice), "%s", text); - world->notice_seconds = 1.25f; + world->notice_seconds = PH_NOTICE_SECONDS; } static const PhEntity * @@ -178,33 +174,25 @@ ph_try_pickup(PhWorld *world, PhEntity *entity) for (i = 0; i < world->ground_item_count; ++i) { PhGroundItem *drop = &world->ground_items[i]; - const PhItemDef *item; + int item_index; if (!drop->active) { continue; } - if (ph_dist2(entity->pos, drop->pos) > 18.0f * 18.0f) { + if (ph_dist2(entity->pos, drop->pos) > PH_PICKUP_RADIUS * PH_PICKUP_RADIUS) { continue; } - item = ph_world_item_def(world, drop->item_id); - if (item) { - world->player_talent_points += item->talent_points * drop->amount; - } + item_index = ph_item_def_index(world, drop->item_id); + if (item_index >= 0) world->inventory[item_index] += drop->amount; drop->active = 0; } } static PhInput -ph_orthogonal_input(PhInput input, const PhEntity *entity) +ph_orthogonal_input(PhInput input) { - if (input.move_x == 0 || input.move_y == 0) { - return input; - } - - if (entity->facing_x != 0) { - input.move_y = 0; - } else { + if (input.move_y != 0) { input.move_x = 0; } return input; @@ -341,7 +329,7 @@ ph_world_drop_item(PhWorld *world, int item_id, PhVec2 pos, int amount) if (world->ground_item_count >= PH_MAX_GROUND_ITEMS) { return -1; } - if (!ph_world_item_def(world, item_id)) { + if (amount <= 0 || !ph_world_item_def(world, item_id)) { return -1; } @@ -388,7 +376,7 @@ ph_world_tick(PhWorld *world, PhInput input, float dt) } } - input = ph_orthogonal_input(input, player); + input = ph_orthogonal_input(input); next = player->pos; if (input.move_x != 0 || input.move_y != 0) { float speed = (float)def->move_speed; @@ -439,10 +427,27 @@ ph_world_item_def(const PhWorld *world, int item_id) } int -ph_area_tile_blocked(const PhArea *area, int tx, int ty) +ph_world_item_amount(const PhWorld *world, int item_id) +{ + int i = ph_item_def_index(world, item_id); + return i >= 0 ? world->inventory[i] : 0; +} + +const PhTileDef * +ph_area_tile_def(const PhArea *area, int tx, int ty) { + unsigned char tile; + if (tx < 0 || ty < 0 || tx >= area->width || ty >= area->height) { - return 1; + return NULL; } - return area->tiles[(size_t)ty * (size_t)area->width + (size_t)tx] == '#'; + tile = area->tiles[(size_t)ty * (size_t)area->width + (size_t)tx]; + return tile < LEN(ph_tile_defs) ? &ph_tile_defs[tile] : NULL; +} + +int +ph_area_tile_blocked(const PhArea *area, int tx, int ty) +{ + const PhTileDef *def = ph_area_tile_def(area, tx, ty); + return !def || def->blocks_movement; } diff --git a/src/engine/world.h b/src/engine/world.h @@ -4,6 +4,9 @@ #include <stddef.h> enum { + PH_MAP_MAGIC = 0x50484d50, + PH_MAP_VERSION = 2, + PH_AREA_NAME_MAX = 64, PH_TILE_SIZE = 16, PH_MAX_ENTITY_TYPES = 32, PH_MAX_ITEM_TYPES = 64, @@ -18,18 +21,33 @@ typedef enum { PH_ENTITY_NPC, } PhEntityKind; +enum { + PH_SPRITE_DOWN, + PH_SPRITE_UP, + PH_SPRITE_SIDE, + PH_SPRITE_DIRECTIONS, +}; + typedef struct { float x; float y; } PhVec2; typedef struct { + unsigned char symbol; + int blocks_movement; + int asset_id; + int sprite_tile_x; + int sprite_tile_y; +} PhTileDef; + +typedef struct { int id; const char *name; int max_hp; int move_speed; - int sprite_tile_x; - int sprite_tile_y; + int asset_id; + int sprite_tiles[PH_SPRITE_DIRECTIONS][2]; int blocks_movement; PhEntityKind kind; } PhEntityDef; @@ -39,7 +57,7 @@ typedef struct { const char *name; int value; int power; - int talent_points; + int asset_id; int sprite_tile_x; int sprite_tile_y; } PhItemDef; @@ -86,7 +104,7 @@ typedef struct { int item_def_count; int entity_count; int ground_item_count; - int player_talent_points; + int inventory[PH_MAX_ITEM_TYPES]; char notice[PH_NOTICE_SIZE]; float notice_seconds; } PhWorld; @@ -111,6 +129,8 @@ void ph_world_tick(PhWorld *world, PhInput input, float dt); const PhEntity *ph_world_player(const PhWorld *world); const PhEntityDef *ph_world_entity_def(const PhWorld *world, int type_id); const PhItemDef *ph_world_item_def(const PhWorld *world, int item_id); +int ph_world_item_amount(const PhWorld *world, int item_id); +const PhTileDef *ph_area_tile_def(const PhArea *area, int tx, int ty); int ph_area_tile_blocked(const PhArea *area, int tx, int ty); #endif diff --git a/src/game/main.c b/src/game/main.c @@ -8,48 +8,23 @@ #include <SDL3/SDL.h> #endif -enum { - PH_VIEW_W = 320, - PH_VIEW_H = 240, - PH_SCALE = 3, -}; - -enum { - PH_DEF_PLAYER = 1, - PH_DEF_SLIME, -}; - -enum { - PH_ITEM_TALENT_SHARD = 1, -}; - -#define PH_START_MAP_PATH "obj/maps/ashen-meadow.phmap" -#define PH_TILESET_PATH "obj/assets/overworld_tileset_grass.bmp" -#define PH_HERO_PATH "obj/assets/hero.bmp" -#define PH_ITEMS_PATH "obj/assets/items.bmp" -#define PH_CRITTERS_PATH "obj/assets/critters.bmp" -#define PH_RENDER_SMOKE_PATH "obj/render-smoke.bmp" - -enum { - PH_TILESET_COLUMNS = 12, - PH_ITEMS_COLUMNS = 8, - PH_TILE_GRASS = 13, - PH_TILE_BLOCKED = 61, -}; +#include "config.h" + +#define LEN(a) (sizeof(a) / sizeof(0[a])) #if PH_USE_SDL typedef struct { - SDL_Texture *tileset; - SDL_Texture *hero; - SDL_Texture *items; - SDL_Texture *critters; + SDL_Texture *textures[LEN(ph_asset_defs)]; } PhAssets; #endif +#if PH_USE_SDL static int ph_make_world(PhWorld *world) { PhArea area; + size_t i; + int entity; int player; if (ph_area_load(&area, PH_START_MAP_PATH) < 0) { @@ -58,75 +33,85 @@ ph_make_world(PhWorld *world) } ph_world_init(world, area, (float)PH_VIEW_W, (float)PH_VIEW_H); - ph_world_add_entity_def(world, (PhEntityDef){ - .id = PH_DEF_PLAYER, - .name = "Wayfarer", - .max_hp = 40, - .move_speed = 90, - .sprite_tile_x = 0, - .sprite_tile_y = 2, - .blocks_movement = 0, - .kind = PH_ENTITY_PLAYER, - }); - ph_world_add_entity_def(world, (PhEntityDef){ - .id = PH_DEF_SLIME, - .name = "Mire Slime", - .max_hp = 12, - .move_speed = 45, - .sprite_tile_x = 12, - .sprite_tile_y = 0, - .blocks_movement = 1, - .kind = PH_ENTITY_MONSTER, - }); - ph_world_add_item_def(world, (PhItemDef){ - .id = PH_ITEM_TALENT_SHARD, - .name = "Talent Shard", - .value = 30, - .power = 0, - .talent_points = 1, - .sprite_tile_x = 2, - .sprite_tile_y = 6, - }); - - player = ph_world_spawn_entity(world, PH_DEF_PLAYER, (PhVec2){ 80.0f, 80.0f }); - ph_world_spawn_entity(world, PH_DEF_SLIME, (PhVec2){ 180.0f, 120.0f }); - ph_world_drop_item(world, PH_ITEM_TALENT_SHARD, (PhVec2){ 104.0f, 80.0f }, 1); + for (i = 0; i < LEN(ph_entity_defs); ++i) + if (ph_world_add_entity_def(world, ph_entity_defs[i]) < 0) goto fail; + for (i = 0; i < LEN(ph_item_defs); ++i) + if (ph_world_add_item_def(world, ph_item_defs[i]) < 0) goto fail; + + player = -1; + for (i = 0; i < LEN(ph_entity_spawns); ++i) { + entity = ph_world_spawn_entity(world, ph_entity_spawns[i].type_id, + ph_entity_spawns[i].pos); + if (entity < 0) goto fail; + if (i == PH_PLAYER_SPAWN) player = entity; + } + if (player < 0) goto fail; + for (i = 0; i < LEN(ph_item_drops); ++i) + if (ph_world_drop_item(world, ph_item_drops[i].item_id, + ph_item_drops[i].pos, ph_item_drops[i].amount) < 0) goto fail; ph_world_set_player(world, player); return 0; + +fail: + fprintf(stderr, "failed to initialize world\n"); + ph_area_free(&world->area); + return -1; } +#endif static int ph_run_smoke_test(void) { + unsigned char tiles[12]; + PhArea area = { "Smoke", 4, 3, tiles, 0 }; PhWorld world; - PhInput input = { .move_x = 1, .move_y = 0, .interact = 0 }; const PhEntity *player; + int open = -1; + int blocked = -1; + size_t tile; + int player_index; int i; - if (ph_make_world(&world) < 0) { + for (tile = 0; tile < LEN(ph_tile_defs); ++tile) { + if (ph_tile_defs[tile].blocks_movement) blocked = (int)tile; + else open = (int)tile; + } + if (open < 0 || blocked < 0) { + fprintf(stderr, "smoke test failed: tile configuration\n"); + return 1; + } + memset(tiles, blocked, sizeof(tiles)); + tiles[5] = tiles[6] = (unsigned char)open; + ph_world_init(&world, area, 64.0f, 48.0f); + if (ph_world_add_entity_def(&world, (PhEntityDef){ + .id = 1, .name = "Tester", .max_hp = 1, .move_speed = 1, + .kind = PH_ENTITY_PLAYER }) < 0 || + ph_world_add_item_def(&world, (PhItemDef){ .id = 1 }) < 0 || + (player_index = ph_world_spawn_entity(&world, 1, + (PhVec2){ 24.0f, 24.0f })) < 0 || + ph_world_drop_item(&world, 1, (PhVec2){ 24.0f, 24.0f }, 1) < 0) { + fprintf(stderr, "smoke test failed: setup\n"); return 1; } + ph_world_set_player(&world, player_index); + ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); for (i = 0; i < 30; ++i) { - input.interact = i == 20; - ph_world_tick(&world, input, 1.0f / 60.0f); + ph_world_tick(&world, (PhInput){ .move_x = 1 }, 1.0f); } + ph_world_tick(&world, (PhInput){ .move_x = 1, .move_y = 1 }, 1.0f); player = ph_world_player(&world); - if (!player) { - fprintf(stderr, "smoke test failed: no player\n"); + if (!player || player->pos.x != 47.0f || player->pos.y != 25.0f || + world.camera.pos.x != 0.0f || world.camera.pos.y != 0.0f || + ph_world_item_amount(&world, 1) != 1) { + fprintf(stderr, "smoke test failed: unexpected world state\n"); return 1; } - printf("area=%s player=(%.1f,%.1f) camera=(%.1f,%.1f) talent=%d\n", - world.area.name, - player->pos.x, - player->pos.y, - world.camera.pos.x, - world.camera.pos.y, - world.player_talent_points); - ph_area_free(&world.area); + printf("smoke player=(%.1f,%.1f) item=%d\n", + player->pos.x, player->pos.y, ph_world_item_amount(&world, 1)); return 0; } @@ -155,31 +140,28 @@ ph_load_sprite_sheet(SDL_Renderer *renderer, const char *path, Uint8 key_r, Uint static int ph_load_assets(PhAssets *assets, SDL_Renderer *renderer) { - memset(assets, 0, sizeof(*assets)); + size_t i; - assets->tileset = ph_load_sprite_sheet(renderer, PH_TILESET_PATH, 0, 0, 0); - assets->hero = ph_load_sprite_sheet(renderer, PH_HERO_PATH, 0, 0, 0); - assets->items = ph_load_sprite_sheet(renderer, PH_ITEMS_PATH, 111, 119, 109); - assets->critters = ph_load_sprite_sheet(renderer, PH_CRITTERS_PATH, 129, 255, 94); - - if (!assets->tileset || !assets->hero || !assets->items || !assets->critters) { - SDL_DestroyTexture(assets->tileset); - SDL_DestroyTexture(assets->hero); - SDL_DestroyTexture(assets->items); - SDL_DestroyTexture(assets->critters); - memset(assets, 0, sizeof(*assets)); - return -1; + memset(assets, 0, sizeof(*assets)); + for (i = 1; i < LEN(ph_asset_defs); ++i) { + if (!ph_asset_defs[i].path) continue; + assets->textures[i] = ph_load_sprite_sheet(renderer, ph_asset_defs[i].path, + ph_asset_defs[i].key_r, ph_asset_defs[i].key_g, ph_asset_defs[i].key_b); + if (!assets->textures[i]) { + while (i > 0) SDL_DestroyTexture(assets->textures[--i]); + return -1; + } } - return 0; + return assets->textures[PH_ASSET_TILESET] ? 0 : -1; } static void ph_destroy_assets(PhAssets *assets) { - SDL_DestroyTexture(assets->tileset); - SDL_DestroyTexture(assets->hero); - SDL_DestroyTexture(assets->items); - SDL_DestroyTexture(assets->critters); + size_t i; + + for (i = 1; i < LEN(assets->textures); ++i) + SDL_DestroyTexture(assets->textures[i]); memset(assets, 0, sizeof(*assets)); } @@ -198,25 +180,16 @@ ph_draw_sprite(SDL_Renderer *renderer, SDL_Texture *texture, } static void -ph_player_sprite_frame(const PhEntityDef *def, const PhEntity *entity, +ph_entity_sprite_frame(const PhEntityDef *def, const PhEntity *entity, int *sprite_tile_x, int *sprite_tile_y, SDL_FlipMode *flip) { - if (entity->facing_y > 0) { - *sprite_tile_x = def->sprite_tile_x + 4; - *sprite_tile_y = 0; - *flip = SDL_FLIP_NONE; - return; - } - if (entity->facing_y < 0) { - *sprite_tile_x = def->sprite_tile_x; - *sprite_tile_y = 0; - *flip = SDL_FLIP_NONE; - return; - } - - *sprite_tile_x = def->sprite_tile_x; - *sprite_tile_y = 1; - *flip = entity->facing_x > 0 ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; + int direction = entity->facing_y > 0 ? PH_SPRITE_DOWN : + entity->facing_y < 0 ? PH_SPRITE_UP : PH_SPRITE_SIDE; + + *sprite_tile_x = def->sprite_tiles[direction][0]; + *sprite_tile_y = def->sprite_tiles[direction][1]; + *flip = direction == PH_SPRITE_SIDE && entity->facing_x > 0 ? + SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; } static void @@ -225,24 +198,34 @@ ph_draw_area(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *worl int tx; int ty; - for (ty = 0; ty < world->area.height; ++ty) { - for (tx = 0; tx < world->area.width; ++tx) { + for (ty = (int)(world->camera.pos.y / PH_TILE_SIZE); + ty < world->area.height && (float)(ty * PH_TILE_SIZE) < + world->camera.pos.y + world->camera.viewport_h; ++ty) { + for (tx = (int)(world->camera.pos.x / PH_TILE_SIZE); + tx < world->area.width && (float)(tx * PH_TILE_SIZE) < + world->camera.pos.x + world->camera.viewport_w; ++tx) { + const PhTileDef *def = ph_area_tile_def(&world->area, tx, ty); + SDL_Texture *texture; SDL_FRect rect = { .x = (float)(tx * PH_TILE_SIZE) - world->camera.pos.x, .y = (float)(ty * PH_TILE_SIZE) - world->camera.pos.y, .w = (float)PH_TILE_SIZE, .h = (float)PH_TILE_SIZE, }; - int tile_id = ph_area_tile_blocked(&world->area, tx, ty) ? - PH_TILE_BLOCKED : PH_TILE_GRASS; - SDL_FRect src = { - .x = (float)((tile_id % PH_TILESET_COLUMNS) * PH_TILE_SIZE), - .y = (float)((tile_id / PH_TILESET_COLUMNS) * PH_TILE_SIZE), + SDL_FRect src; + + if (!def || def->asset_id <= PH_ASSET_NONE || + (size_t)def->asset_id >= LEN(assets->textures)) continue; + texture = assets->textures[def->asset_id]; + if (!texture) continue; + src = (SDL_FRect){ + .x = (float)(def->sprite_tile_x * PH_TILE_SIZE), + .y = (float)(def->sprite_tile_y * PH_TILE_SIZE), .w = (float)PH_TILE_SIZE, .h = (float)PH_TILE_SIZE, }; - SDL_RenderTexture(renderer, assets->tileset, &src, &rect); + SDL_RenderTexture(renderer, texture, &src, &rect); } } } @@ -255,6 +238,7 @@ ph_draw_items(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *wor for (i = 0; i < world->ground_item_count; ++i) { const PhGroundItem *drop = &world->ground_items[i]; const PhItemDef *def = ph_world_item_def(world, drop->item_id); + SDL_Texture *texture = NULL; SDL_FRect rect; if (!drop->active) { @@ -266,15 +250,18 @@ ph_draw_items(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *wor rect.w = 6.0f; rect.h = 6.0f; - if (def && def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0) { + if (def && def->asset_id > PH_ASSET_NONE && + (size_t)def->asset_id < LEN(assets->textures)) + texture = assets->textures[def->asset_id]; + if (texture && def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0) { rect.x = drop->pos.x - world->camera.pos.x - 8.0f; rect.y = drop->pos.y - world->camera.pos.y - 8.0f; rect.w = 16.0f; rect.h = 16.0f; - ph_draw_sprite(renderer, assets->items, + ph_draw_sprite(renderer, texture, def->sprite_tile_x, def->sprite_tile_y, rect, SDL_FLIP_NONE); } else { - SDL_SetRenderDrawColor(renderer, 242, 214, 80, 255); + SDL_SetRenderDrawColor(renderer, PH_ITEM_COLOR); SDL_RenderFillRect(renderer, &rect); } } @@ -288,28 +275,28 @@ ph_draw_entities(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld * for (i = 0; i < world->entity_count; ++i) { const PhEntity *entity = &world->entities[i]; const PhEntityDef *def = ph_world_entity_def(world, entity->type_id); + SDL_Texture *texture = NULL; SDL_FRect rect; + int sprite_tile_x; + int sprite_tile_y; + SDL_FlipMode flip; if (!entity->active || !def) { continue; } - if (def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0 && - (def->kind == PH_ENTITY_PLAYER || def->kind == PH_ENTITY_MONSTER)) { - SDL_Texture *sheet = def->kind == PH_ENTITY_PLAYER ? assets->hero : assets->critters; - int sprite_tile_x = def->sprite_tile_x; - int sprite_tile_y = def->sprite_tile_y; - SDL_FlipMode flip = SDL_FLIP_NONE; + if (def->asset_id > PH_ASSET_NONE && + (size_t)def->asset_id < LEN(assets->textures)) + texture = assets->textures[def->asset_id]; + ph_entity_sprite_frame(def, entity, &sprite_tile_x, &sprite_tile_y, &flip); + if (texture && sprite_tile_x >= 0 && sprite_tile_y >= 0) { rect.x = entity->pos.x - world->camera.pos.x - 8.0f; rect.y = entity->pos.y - world->camera.pos.y - 12.0f; rect.w = 16.0f; rect.h = 16.0f; - if (def->kind == PH_ENTITY_PLAYER) { - ph_player_sprite_frame(def, entity, &sprite_tile_x, &sprite_tile_y, &flip); - } - ph_draw_sprite(renderer, sheet, sprite_tile_x, sprite_tile_y, rect, flip); + ph_draw_sprite(renderer, texture, sprite_tile_x, sprite_tile_y, rect, flip); } else { rect.x = entity->pos.x - world->camera.pos.x - 7.0f; rect.y = entity->pos.y - world->camera.pos.y - 12.0f; @@ -317,9 +304,9 @@ ph_draw_entities(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld * rect.h = 18.0f; if (def->kind == PH_ENTITY_MONSTER) { - SDL_SetRenderDrawColor(renderer, 168, 108, 224, 255); + SDL_SetRenderDrawColor(renderer, PH_MONSTER_COLOR); } else { - SDL_SetRenderDrawColor(renderer, 220, 180, 90, 255); + SDL_SetRenderDrawColor(renderer, PH_NPC_COLOR); } SDL_RenderFillRect(renderer, &rect); } @@ -331,7 +318,7 @@ ph_render_frame(SDL_Renderer *renderer, const PhAssets *assets, PhWorld *world, { ph_world_tick(world, input, dt); - SDL_SetRenderDrawColor(renderer, 16, 18, 24, 255); + SDL_SetRenderDrawColor(renderer, PH_CLEAR_COLOR); SDL_RenderClear(renderer); ph_draw_area(renderer, assets, world); ph_draw_items(renderer, assets, world); @@ -344,9 +331,9 @@ ph_render_frame(SDL_Renderer *renderer, const PhAssets *assets, PhWorld *world, .h = 20.0f, }; - SDL_SetRenderDrawColor(renderer, 20, 24, 34, 255); + SDL_SetRenderDrawColor(renderer, PH_NOTICE_COLOR); SDL_RenderFillRect(renderer, &notice_bg); - SDL_SetRenderDrawColor(renderer, 232, 236, 244, 255); + SDL_SetRenderDrawColor(renderer, PH_NOTICE_TEXT_COLOR); SDL_RenderDebugText(renderer, 14.0f, (float)(PH_VIEW_H - 22), world->notice); } SDL_RenderPresent(renderer); @@ -357,19 +344,19 @@ ph_read_input(const bool *keys) { PhInput input = { 0 }; - if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_A]) { + if (keys[PH_KEY_LEFT_1] || keys[PH_KEY_LEFT_2]) { input.move_x -= 1; } - if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_D]) { + if (keys[PH_KEY_RIGHT_1] || keys[PH_KEY_RIGHT_2]) { input.move_x += 1; } - if (keys[SDL_SCANCODE_UP] || keys[SDL_SCANCODE_W]) { + if (keys[PH_KEY_UP_1] || keys[PH_KEY_UP_2]) { input.move_y -= 1; } - if (keys[SDL_SCANCODE_DOWN] || keys[SDL_SCANCODE_S]) { + if (keys[PH_KEY_DOWN_1] || keys[PH_KEY_DOWN_2]) { input.move_y += 1; } - if (keys[SDL_SCANCODE_E] || keys[SDL_SCANCODE_SPACE]) { + if (keys[PH_KEY_INTERACT_1] || keys[PH_KEY_INTERACT_2]) { input.interact = 1; } @@ -379,7 +366,7 @@ ph_read_input(const bool *keys) static int ph_create_window_renderer(SDL_Window **window, SDL_Renderer **renderer) { - *window = SDL_CreateWindow("phantasia", + *window = SDL_CreateWindow(PH_WINDOW_TITLE, PH_VIEW_W * PH_SCALE, PH_VIEW_H * PH_SCALE, 0); @@ -411,6 +398,7 @@ ph_run_render_smoke_test(void) PhAssets assets; PhWorld world; int i; + int result = 0; if (!SDL_Init(SDL_INIT_VIDEO)) { fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); @@ -453,6 +441,7 @@ ph_run_render_smoke_test(void) if (!SDL_SaveBMP(surface, PH_RENDER_SMOKE_PATH)) { fprintf(stderr, "SDL_SaveBMP failed: %s\n", SDL_GetError()); + result = 1; } ph_destroy_assets(&assets); @@ -460,7 +449,7 @@ ph_run_render_smoke_test(void) SDL_DestroyRenderer(renderer); SDL_DestroySurface(surface); SDL_Quit(); - return 0; + return result; } static int @@ -518,8 +507,8 @@ ph_run_game(int frame_limit) now_ticks = SDL_GetTicks(); dt = (float)(now_ticks - last_ticks) / 1000.0f; - if (dt > 0.05f) { - dt = 0.05f; + if (dt > PH_MAX_FRAME_TIME) { + dt = PH_MAX_FRAME_TIME; } last_ticks = now_ticks; @@ -529,7 +518,7 @@ ph_run_game(int frame_limit) if (frame_limit > 0 && frame_count >= frame_limit) { running = 0; } - SDL_Delay(16); + SDL_Delay(PH_FRAME_DELAY_MS); } ph_destroy_assets(&assets); diff --git a/src/tools/mapc.c b/src/tools/mapc.c @@ -1,12 +1,14 @@ +#include "engine/world.h" +#include "config.h" + #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#define LEN(a) (sizeof(a) / sizeof(0[a])) + enum { - PH_MAP_MAGIC = 0x50484d50, - PH_MAP_VERSION = 1, - PH_AREA_NAME_MAX = 64, PH_MAP_LINE_MAX = 512, }; @@ -18,6 +20,16 @@ typedef struct { char name[PH_AREA_NAME_MAX]; } PhMapHeader; +static int +ph_tile_index(unsigned char symbol) +{ + size_t i; + + for (i = 0; i < LEN(ph_tile_defs); ++i) + if (ph_tile_defs[i].symbol == symbol) return (int)i; + return -1; +} + static void ph_strip_newline(char *line) { @@ -43,6 +55,11 @@ ph_compile_map(const char *src_path, const char *dst_path) size_t tile_count = 0; int in_tiles = 0; + if (LEN(ph_tile_defs) > 256) { + fprintf(stderr, "too many configured tile types\n"); + return 1; + } + src = fopen(src_path, "r"); if (!src) { perror(src_path); @@ -51,6 +68,7 @@ ph_compile_map(const char *src_path, const char *dst_path) while (fgets(line, sizeof(line), src)) { size_t len; + size_t x; ph_strip_newline(line); if (!in_tiles && strncmp(line, "name:", 5) == 0) { @@ -79,6 +97,18 @@ ph_compile_map(const char *src_path, const char *dst_path) } len = strlen(line); + for (x = 0; x < len; ++x) { + int tile = ph_tile_index((unsigned char)line[x]); + + if (tile < 0) { + fprintf(stderr, "%s: unknown tile '%c' in row %u\n", + src_path, line[x], header.height + 1); + free(tiles); + fclose(src); + return 1; + } + line[x] = (char)tile; + } if (header.width == 0) { header.width = (uint32_t)len; } else if (len != header.width) {