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:
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, ¬ice_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) {