commit 60ea9097738a1caea17a654d1ab1c720d9c3b954
parent 4d71e647f0f4daa1d931fe03eb6a0d9d32345e56
Author: beep <beep@wimdupont.com>
Date: Thu, 30 Jul 2026 07:27:41 +0000
Move world placement into map markers
Diffstat:
13 files changed, 363 insertions(+), 106 deletions(-)
diff --git a/Makefile b/Makefile
@@ -3,7 +3,7 @@ SMOKE = bin/ph-smoke
MAPC = bin/ph-mapc
OBJDIR = obj
MAPDIR = $(OBJDIR)/maps
-MAPSRC = data/maps/ashen-meadow.txt data/maps/stonehollow.txt
+MAPSRC = $(wildcard data/maps/*.txt)
MAPBIN = $(MAPSRC:data/maps/%.txt=$(MAPDIR)/%.phmap)
TILESET_SRC = assets/vendor/overworld-grass-biome/TilesetGrass/overworld_tileset_grass.png
TILESET_BMP = $(OBJDIR)/assets/overworld_tileset_grass.bmp
diff --git a/README.adoc b/README.adoc
@@ -11,20 +11,28 @@
* 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.
+Phantasia is a game, not a general-purpose engine product. "Engine-like" means
+that its systems operate on generic concepts such as entities, items,
+interactions, and map markers instead of named content such as Mira or
+Stonehollow. Named IDs belong in the content catalog and content-specific tests;
+initialization, world logic, and presentation treat those IDs as opaque data.
+
Core world and CTB battle logic are independent of SDL. `src/game/main.c`
composes those systems with input and presentation; `src/game/battle.c` owns the
battle state machine. Headless world and battle coverage lives separately in
`src/tests/smoke.c`; production `main.c` contains no logic-test fixtures.
`src/game/area.c` loads content-defined maps and portals while preserving the
-state of entities and ground items belonging to inactive areas.
+state of entities and ground items belonging to inactive areas. Marker content
+is instantiated once when an area is first entered; only the active tile map is
+retained.
World actors update even while the player stands still. Entity content defines
relative movement speed, directional animation frames, vision, and aggression;
-spawn content defines each actor's home and wandering radius. Aggressive actors
-with clear sight pursue the player, remember the last visible position briefly,
-then return to their territory. Movement and sight both respect blocked map
-tiles, while reserved destinations keep moving entities from crossing through
-one another.
+map-marker content defines each actor's home and wandering radius. Aggressive
+actors with clear sight pursue the player, remember the last visible position
+briefly, then return to their territory. Movement and sight both respect blocked
+map tiles, while reserved destinations keep moving entities from crossing
+through one another.
== Principles
@@ -65,12 +73,16 @@ replace `config.h` with it if no local customizations need to be retained.
Configuration owns compile-time settings such as display dimensions, engine
radii and timing, paths, colors, and controls. Concrete game content—assets,
-tiles, entities, items, spells, starting equipment, spawns, drops, and rewards—
-lives in the typed `src/game/content.c`/`content.h` module. Adding content should
-extend those data tables without adding renderer or initialization special cases.
-Entity spawns also own instance-specific territory radii; global world movement
-pace, actor decision timing, wandering chance, and pursuit memory remain settings
-in `config.h`.
+tiles, entities, items, spells, starting equipment, map-marker meanings, and
+rewards—lives in the typed `src/game/content.c`/`content.h` module. Maps own
+placement: their symbols place players, monsters, NPCs, items, portals, and
+arrival points. Content definitions map each symbol to a generic marker kind and
+its underlying terrain tile. Adding an instance should normally change only a
+map; adding a new content definition should change the content catalog and map
+without adding renderer or initialization special cases. Marker definitions also
+own instance-specific values such as territory radius or item amount; global
+world movement pace, actor decision timing, wandering chance, and pursuit memory
+remain settings in `config.h`.
Items define distinct buy and sell prices. The player carries gold, and shop
content defines stock and dialogue without embedding merchant-specific rules in
@@ -128,6 +140,18 @@ Current tile legend across the meadow and Stonehollow town maps:
* `.` = walkable ground
* `#` = blocked wall
* `,` = walkable town path
-* `B` = blocked building wall
-* `=` = walkable building floor or doorway
-* `T` = walkable area portal
+* `[`, `^`, `]`, `{`, `}`, `(`, `_`, `)` = blocked building parts
+* `=` and `+` = walkable building floor or doorway
+
+Current content-marker legend:
+
+* `@` = player spawn
+* `a` = slime spawn
+* `m` = Mira spawn
+* `1` and `2` = item drops
+* `>` and `<` = area portals
+* `e` = portal arrival point
+
+Marker meanings are area-specific entries in `content.c`, not engine rules. The
+map compiler records their positions and replaces them with the configured
+underlying terrain, so marker symbols never become visible tiles at runtime.
diff --git a/data/maps/ashen-meadow.txt b/data/maps/ashen-meadow.txt
@@ -2,11 +2,11 @@ name: Ashen Meadow
################################
#..............................#
-#............................T.#
+#...........................e>.#
#......####....................#
-#......#..#....................#
-#......####............###.....#
-#......................#.......#
+#....@1#..#....................#
+#.....2####............###.....#
+#..........a...........#.......#
#......................#.......#
#......................###.....#
#..............................#
diff --git a/data/maps/stonehollow.txt b/data/maps/stonehollow.txt
@@ -4,7 +4,7 @@ name: Stonehollow
#..............................#
#..[^^^^^^]....[^^^^^^]........#
#..{======}....{======}........#
-#..{======}....{======}........#
+#..{=m====}....{======}........#
#..{======}....{======}........#
#..(__+___)....(___+__)........#
#.....,............,...........#
@@ -21,6 +21,6 @@ name: Stonehollow
#...........,,,,,..............#
#..............,...............#
#..............,...............#
-#..............,...............#
-#..............T...............#
+#..............e...............#
+#..............<...............#
################################
diff --git a/src/engine/world.c b/src/engine/world.c
@@ -2,6 +2,7 @@
#include "config.h"
#include <stdint.h>
+#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
@@ -24,9 +25,16 @@ typedef struct {
uint32_t version;
uint32_t width;
uint32_t height;
+ uint32_t marker_count;
char name[PH_AREA_NAME_MAX];
} PhMapHeader;
+typedef struct {
+ uint32_t symbol;
+ uint32_t tile_x;
+ uint32_t tile_y;
+} PhMapMarker;
+
static float
ph_clampf(float value, float min, float max)
{
@@ -480,7 +488,9 @@ ph_area_load(PhArea *area, const char *path)
size_t tile_count;
size_t name_len;
unsigned char *tiles;
+ PhAreaMarker *markers = NULL;
char *name;
+ uint32_t i;
memset(area, 0, sizeof(*area));
@@ -493,12 +503,18 @@ ph_area_load(PhArea *area, const char *path)
header.magic != PH_MAP_MAGIC ||
header.version != PH_MAP_VERSION ||
header.width == 0 ||
- header.height == 0) {
+ header.height == 0 || header.width > INT_MAX ||
+ header.height > INT_MAX || header.marker_count > INT_MAX ||
+ (size_t)header.width > SIZE_MAX / header.height) {
fclose(fp);
return -1;
}
tile_count = (size_t)header.width * (size_t)header.height;
+ if (header.marker_count > tile_count) {
+ fclose(fp);
+ return -1;
+ }
tiles = malloc(tile_count);
if (!tiles) {
fclose(fp);
@@ -510,12 +526,35 @@ ph_area_load(PhArea *area, const char *path)
fclose(fp);
return -1;
}
+ if (header.marker_count > 0) {
+ markers = calloc(header.marker_count, sizeof(*markers));
+ if (!markers) {
+ free(tiles);
+ fclose(fp);
+ return -1;
+ }
+ for (i = 0; i < header.marker_count; ++i) {
+ PhMapMarker marker;
+
+ if (fread(&marker, sizeof(marker), 1, fp) != 1 ||
+ marker.symbol > 255 || marker.tile_x >= header.width ||
+ marker.tile_y >= header.height) {
+ free(markers);
+ free(tiles);
+ fclose(fp);
+ return -1;
+ }
+ markers[i] = (PhAreaMarker){ (unsigned char)marker.symbol,
+ (int)marker.tile_x, (int)marker.tile_y };
+ }
+ }
fclose(fp);
header.name[PH_AREA_NAME_MAX - 1] = '\0';
name_len = strlen(header.name) + 1;
name = malloc(name_len);
if (!name) {
+ free(markers);
free(tiles);
return -1;
}
@@ -525,6 +564,8 @@ ph_area_load(PhArea *area, const char *path)
area->width = (int)header.width;
area->height = (int)header.height;
area->tiles = tiles;
+ area->markers = markers;
+ area->marker_count = (int)header.marker_count;
area->owns_tiles = 1;
return 0;
}
@@ -534,6 +575,7 @@ ph_area_free(PhArea *area)
{
if (area->owns_tiles) {
free(area->tiles);
+ free(area->markers);
free((char *)area->name);
}
memset(area, 0, sizeof(*area));
diff --git a/src/engine/world.h b/src/engine/world.h
@@ -7,11 +7,12 @@
enum {
PH_MAP_MAGIC = 0x50484d50,
- PH_MAP_VERSION = 2,
+ PH_MAP_VERSION = 3,
PH_AREA_NAME_MAX = 64,
PH_TILE_SIZE = 16,
PH_MAX_ENTITY_TYPES = 32,
PH_MAX_ITEM_TYPES = 64,
+ PH_MAX_AREAS = 32,
PH_MAX_ENTITIES = 128,
PH_MAX_GROUND_ITEMS = 128,
PH_NOTICE_SIZE = 96,
@@ -140,10 +141,18 @@ typedef struct {
} PhGroundItem;
typedef struct {
+ unsigned char symbol;
+ int tile_x;
+ int tile_y;
+} PhAreaMarker;
+
+typedef struct {
const char *name;
int width;
int height;
unsigned char *tiles;
+ PhAreaMarker *markers;
+ int marker_count;
int owns_tiles;
const PhTileDef *tile_defs;
int tile_def_count;
@@ -169,6 +178,8 @@ typedef struct {
int entity_count;
int ground_item_count;
int inventory[PH_MAX_ITEM_TYPES];
+ int initialized_areas[PH_MAX_AREAS];
+ int initialized_area_count;
int encounter_index;
int interaction_id;
unsigned int rng_state;
diff --git a/src/game/area.c b/src/game/area.c
@@ -24,6 +24,17 @@ ph_game_shop_def(int shop_id)
return NULL;
}
+const PhMarkerDef *
+ph_game_marker_def(int area_id, unsigned char symbol)
+{
+ size_t i;
+
+ for (i = 0; i < LEN(ph_marker_defs); ++i)
+ if (ph_marker_defs[i].area_id == area_id &&
+ ph_marker_defs[i].symbol == symbol) return &ph_marker_defs[i];
+ return NULL;
+}
+
const PhInteractionDef *
ph_game_interaction_def(int interaction_id)
{
@@ -46,6 +57,62 @@ ph_game_load_area(PhArea *area, int area_id)
return 0;
}
+static PhVec2
+ph_marker_pos(const PhAreaMarker *marker)
+{
+ return (PhVec2){ PH_TILE_CENTER(marker->tile_x),
+ PH_TILE_CENTER(marker->tile_y) };
+}
+
+static int
+ph_game_add_area_content(PhWorld *world, const PhArea *area, int area_id,
+ int *player)
+{
+ int i;
+
+ for (i = 0; i < area->marker_count; ++i) {
+ const PhAreaMarker *marker = &area->markers[i];
+ const PhMarkerDef *def = ph_game_marker_def(area_id, marker->symbol);
+ int entity;
+
+ if (!def) return -1;
+ if (def->kind == PH_MARKER_ITEM) {
+ if (ph_world_drop_item(world, def->content_id, ph_marker_pos(marker),
+ def->amount, area_id) < 0) return -1;
+ continue;
+ }
+ if (def->kind != PH_MARKER_ENTITY && def->kind != PH_MARKER_PLAYER)
+ continue;
+ if (def->kind == PH_MARKER_PLAYER && *player >= 0) return -1;
+ entity = ph_world_spawn_entity(world, def->content_id,
+ ph_marker_pos(marker), def->territory_radius, area_id);
+ if (entity < 0) return -1;
+ if (def->kind == PH_MARKER_PLAYER) *player = entity;
+ }
+ return 0;
+}
+
+static int
+ph_game_prepare_area(PhWorld *world, const PhArea *area, int area_id, int *player)
+{
+ int entity_count = world->entity_count;
+ int ground_item_count = world->ground_item_count;
+ int old_player = *player;
+ int i;
+
+ for (i = 0; i < world->initialized_area_count; ++i)
+ if (world->initialized_areas[i] == area_id) return 0;
+ if (world->initialized_area_count >= PH_MAX_AREAS ||
+ ph_game_add_area_content(world, area, area_id, player) < 0) {
+ world->entity_count = entity_count;
+ world->ground_item_count = ground_item_count;
+ *player = old_player;
+ return -1;
+ }
+ world->initialized_areas[world->initialized_area_count++] = area_id;
+ return 0;
+}
+
int
ph_game_world_init(PhWorld *world, float viewport_w, float viewport_h)
{
@@ -59,19 +126,9 @@ ph_game_world_init(PhWorld *world, float viewport_w, float viewport_h)
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;
- for (i = 0; i < LEN(ph_entity_spawns); ++i) {
- int entity = ph_world_spawn_entity(world, ph_entity_spawns[i].type_id,
- ph_entity_spawns[i].pos, ph_entity_spawns[i].territory_radius,
- ph_entity_spawns[i].area_id);
-
- if (entity < 0) goto fail;
- if (i == PH_PLAYER_SPAWN) player = entity;
- }
+ if (ph_game_prepare_area(world, &world->area, PH_START_AREA, &player) < 0)
+ goto fail;
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,
- ph_item_drops[i].area_id) < 0) goto fail;
for (i = 0; i < LEN(ph_player_equipment); ++i)
if (ph_player_equipment[i] &&
ph_world_equip_item(world, player, ph_player_equipment[i]) < 0)
@@ -90,20 +147,42 @@ ph_game_update_portal(PhWorld *world)
const PhEntity *player = ph_world_player(world);
int tile_x;
int tile_y;
- size_t i;
+ int i;
if (!player) return -1;
tile_x = (int)floorf(player->pos.x / PH_TILE_SIZE);
tile_y = (int)floorf(player->pos.y / PH_TILE_SIZE);
- for (i = 0; i < LEN(ph_portal_defs); ++i) {
- const PhPortalDef *portal = &ph_portal_defs[i];
+ for (i = 0; i < world->area.marker_count; ++i) {
+ const PhAreaMarker *marker = &world->area.markers[i];
+ const PhMarkerDef *portal = ph_game_marker_def(world->area_id,
+ marker->symbol);
PhArea area;
+ PhVec2 destination;
+ int player_index = world->player_index;
+ int j;
- if (portal->area_id != world->area_id || portal->tile_x != tile_x ||
- portal->tile_y != tile_y) continue;
+ if (!portal || portal->kind != PH_MARKER_PORTAL ||
+ marker->tile_x != tile_x || marker->tile_y != tile_y) continue;
if (ph_game_load_area(&area, portal->destination_area_id) < 0) return -1;
+ for (j = 0; j < area.marker_count; ++j) {
+ const PhMarkerDef *arrival = ph_game_marker_def(
+ portal->destination_area_id, area.markers[j].symbol);
+
+ if (area.markers[j].symbol == portal->destination_symbol && arrival &&
+ arrival->kind == PH_MARKER_ARRIVAL) break;
+ }
+ if (j == area.marker_count) {
+ ph_area_free(&area);
+ return -1;
+ }
+ if (ph_game_prepare_area(world, &area, portal->destination_area_id,
+ &player_index) < 0) {
+ ph_area_free(&area);
+ return -1;
+ }
+ destination = ph_marker_pos(&area.markers[j]);
ph_world_enter_area(world, area, portal->destination_area_id,
- portal->destination);
+ destination);
return 1;
}
return 0;
diff --git a/src/game/area.h b/src/game/area.h
@@ -6,6 +6,7 @@
int ph_game_world_init(PhWorld *world, float viewport_w, float viewport_h);
int ph_game_update_portal(PhWorld *world);
const PhAreaDef *ph_game_area_def(int area_id);
+const PhMarkerDef *ph_game_marker_def(int area_id, unsigned char symbol);
const PhShopDef *ph_game_shop_def(int shop_id);
const PhInteractionDef *ph_game_interaction_def(int interaction_id);
diff --git a/src/game/content.c b/src/game/content.c
@@ -26,7 +26,6 @@ const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = {
{ '_', 1, PH_ASSET_TILESET, 10, 3 },
{ ')', 1, PH_ASSET_TILESET, 11, 3 },
{ '+', 0, PH_ASSET_TILESET, 10, 3 },
- { 'T', 0, PH_ASSET_TILESET, 6, 11 },
};
const PhSpellDef ph_spell_defs[PH_SPELL_DEF_COUNT] = {
@@ -160,25 +159,22 @@ const int ph_player_equipment[PH_EQUIP_COUNT] = {
[PH_EQUIP_ARMOR] = PH_ITEM_TRAVELER_COAT,
};
-const PhEntitySpawn ph_entity_spawns[PH_ENTITY_SPAWN_COUNT] = {
- { PH_AREA_MEADOW, PH_DEF_PLAYER, { PH_TILE_CENTER(5), PH_TILE_CENTER(4) }, 0 },
- { PH_AREA_MEADOW, PH_DEF_SLIME, { PH_TILE_CENTER(11), PH_TILE_CENTER(7) }, 5 },
- { PH_AREA_STONEHOLLOW, PH_DEF_SHOPKEEPER,
- { PH_TILE_CENTER(5), PH_TILE_CENTER(4) }, 0 },
-};
-
-const PhItemDrop ph_item_drops[PH_ITEM_DROP_COUNT] = {
- { PH_AREA_MEADOW, PH_ITEM_RUSTED_SPEAR,
- { PH_TILE_CENTER(6), PH_TILE_CENTER(4) }, 1 },
- { PH_AREA_MEADOW, PH_ITEM_HEALING_POTION,
- { PH_TILE_CENTER(6), PH_TILE_CENTER(5) }, 1 },
-};
-
-const PhPortalDef ph_portal_defs[PH_PORTAL_COUNT] = {
- { PH_AREA_MEADOW, 29, 2, PH_AREA_STONEHOLLOW,
- { PH_TILE_CENTER(15), PH_TILE_CENTER(21) } },
- { PH_AREA_STONEHOLLOW, 15, 22, PH_AREA_MEADOW,
- { PH_TILE_CENTER(28), PH_TILE_CENTER(2) } },
+const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT] = {
+ { PH_AREA_MEADOW, '@', '.', PH_MARKER_PLAYER, PH_DEF_PLAYER, .amount = 0 },
+ { PH_AREA_MEADOW, 'a', '.', PH_MARKER_ENTITY, PH_DEF_SLIME,
+ .territory_radius = 5 },
+ { PH_AREA_MEADOW, '1', '.', PH_MARKER_ITEM,
+ PH_ITEM_RUSTED_SPEAR, .amount = 1 },
+ { PH_AREA_MEADOW, '2', '.', PH_MARKER_ITEM,
+ PH_ITEM_HEALING_POTION, .amount = 1 },
+ { PH_AREA_MEADOW, '>', '.', PH_MARKER_PORTAL,
+ .destination_area_id = PH_AREA_STONEHOLLOW, .destination_symbol = 'e' },
+ { PH_AREA_MEADOW, 'e', '.', PH_MARKER_ARRIVAL, .content_id = 0 },
+ { PH_AREA_STONEHOLLOW, 'm', '=', PH_MARKER_ENTITY,
+ PH_DEF_SHOPKEEPER, .amount = 0 },
+ { PH_AREA_STONEHOLLOW, '<', ',', PH_MARKER_PORTAL,
+ .destination_area_id = PH_AREA_MEADOW, .destination_symbol = 'e' },
+ { PH_AREA_STONEHOLLOW, 'e', ',', PH_MARKER_ARRIVAL, .content_id = 0 },
};
const PhShopDef ph_shop_defs[PH_SHOP_COUNT] = {
diff --git a/src/game/content.h b/src/game/content.h
@@ -15,27 +15,25 @@ typedef struct {
const char *path;
} PhAreaDef;
-typedef struct {
- int area_id;
- int type_id;
- PhVec2 pos;
- int territory_radius;
-} PhEntitySpawn;
+typedef enum {
+ PH_MARKER_ENTITY,
+ PH_MARKER_PLAYER,
+ PH_MARKER_ITEM,
+ PH_MARKER_PORTAL,
+ PH_MARKER_ARRIVAL,
+} PhMarkerKind;
typedef struct {
int area_id;
- int item_id;
- PhVec2 pos;
+ unsigned char symbol;
+ unsigned char tile_symbol;
+ PhMarkerKind kind;
+ int content_id;
int amount;
-} PhItemDrop;
-
-typedef struct {
- int area_id;
- int tile_x;
- int tile_y;
+ int territory_radius;
int destination_area_id;
- PhVec2 destination;
-} PhPortalDef;
+ unsigned char destination_symbol;
+} PhMarkerDef;
enum { PH_SHOP_ITEM_MAX = 8, PH_SHOP_TALK_LINES = 2 };
@@ -114,11 +112,8 @@ enum {
};
enum {
- PH_TILE_DEF_COUNT = 14,
- PH_ENTITY_SPAWN_COUNT = 3,
- PH_ITEM_DROP_COUNT = 2,
- PH_PORTAL_COUNT = 2,
- PH_PLAYER_SPAWN = 0,
+ PH_TILE_DEF_COUNT = 13,
+ PH_MARKER_DEF_COUNT = 9,
};
extern const PhAssetDef ph_asset_defs[PH_ASSET_COUNT];
@@ -129,9 +124,7 @@ extern const PhItemDef ph_item_defs[PH_ITEM_DEF_COUNT];
extern const PhSpellDef ph_spell_defs[PH_SPELL_DEF_COUNT];
extern const int ph_player_spells[PH_PLAYER_SPELL_COUNT];
extern const int ph_player_equipment[PH_EQUIP_COUNT];
-extern const PhEntitySpawn ph_entity_spawns[PH_ENTITY_SPAWN_COUNT];
-extern const PhItemDrop ph_item_drops[PH_ITEM_DROP_COUNT];
-extern const PhPortalDef ph_portal_defs[PH_PORTAL_COUNT];
+extern const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT];
extern const PhShopDef ph_shop_defs[PH_SHOP_COUNT];
extern const PhInteractionDef ph_interaction_defs[PH_INTERACTION_COUNT];
diff --git a/src/game/main.c b/src/game/main.c
@@ -1178,6 +1178,8 @@ ph_run_render_smoke_test(void)
int enemy_index = -1;
int result = 0;
int order[PH_BATTLE_ORDER_COUNT];
+ int first_item;
+ int second_item;
if (!SDL_Init(SDL_INIT_VIDEO)) {
fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
@@ -1205,6 +1207,16 @@ ph_run_render_smoke_test(void)
SDL_Quit();
return 1;
}
+ if (world.ground_item_count < 2) {
+ fprintf(stderr, "render smoke test failed: item setup\n");
+ ph_area_free(&world.area);
+ SDL_DestroyRenderer(renderer);
+ SDL_DestroySurface(surface);
+ SDL_Quit();
+ return 1;
+ }
+ first_item = world.ground_items[0].item_id;
+ second_item = world.ground_items[1].item_id;
if (ph_load_assets(&assets, renderer) < 0) {
ph_area_free(&world.area);
@@ -1215,7 +1227,7 @@ ph_run_render_smoke_test(void)
}
ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f);
- if (ph_world_item_amount(&world, ph_item_drops[0].item_id) != 0) {
+ if (ph_world_item_amount(&world, first_item) != 0) {
fprintf(stderr, "render smoke test failed: adjacent item pickup\n");
result = 1;
}
@@ -1225,8 +1237,8 @@ ph_run_render_smoke_test(void)
ph_world_tick(&world, (PhInput){ .move_y = 1 },
(float)PH_WORLD_STEP_MILLISECONDS / 1000.0f);
ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f);
- if (ph_world_item_amount(&world, ph_item_drops[0].item_id) != 1 ||
- ph_world_item_amount(&world, ph_item_drops[1].item_id) != 1) {
+ if (ph_world_item_amount(&world, first_item) != 1 ||
+ ph_world_item_amount(&world, second_item) != 1) {
fprintf(stderr, "render smoke test failed: item pickup\n");
result = 1;
}
@@ -1261,7 +1273,7 @@ ph_run_render_smoke_test(void)
i >= 6 ? PH_MENU_INVENTORY : PH_MENU_NONE;
ph_render_frame(renderer, &assets, &context, (PhInput){ 0 }, 1.0f / 60.0f);
}
- ph_shop_open(&shop_ui, PH_SHOP_STONEHOLLOW);
+ ph_shop_open(&shop_ui, ph_shop_defs[0].id);
for (i = PH_SHOP_ROOT; i <= PH_SHOP_TALK; ++i) {
shop_ui.mode = (PhShopMode)i;
shop_ui.selected = i == PH_SHOP_SELL ?
@@ -1351,13 +1363,21 @@ ph_run_render_smoke_test(void)
}
}
game = PH_GAME_WORLD;
- world.entities[world.player_index].pos = (PhVec2){
- PH_TILE_CENTER(29), PH_TILE_CENTER(2) };
- if (ph_game_update_portal(&world) != 1) {
+ for (i = 0; i < world.area.marker_count; ++i) {
+ const PhMarkerDef *def = ph_game_marker_def(world.area_id,
+ world.area.markers[i].symbol);
+
+ if (!def || def->kind != PH_MARKER_PORTAL) continue;
+ world.entities[world.player_index].pos = (PhVec2){
+ PH_TILE_CENTER(world.area.markers[i].tile_x),
+ PH_TILE_CENTER(world.area.markers[i].tile_y) };
+ break;
+ }
+ if (i == world.area.marker_count || ph_game_update_portal(&world) != 1) {
fprintf(stderr, "render smoke test failed: town portal\n");
result = 1;
}
- ph_shop_open(&shop_ui, PH_SHOP_STONEHOLLOW);
+ ph_shop_open(&shop_ui, ph_shop_defs[0].id);
shop_ui.mode = PH_SHOP_TALK;
context.menu = PH_MENU_SHOP;
ph_render_frame(renderer, &assets, &context, (PhInput){ 0 }, 0.0f);
diff --git a/src/tests/smoke.c b/src/tests/smoke.c
@@ -224,6 +224,25 @@ ph_logic_test(int open, int blocked)
}
static int
+ph_enter_portal(PhWorld *world)
+{
+ PhEntity *player = &world->entities[world->player_index];
+ int i;
+
+ for (i = 0; i < world->area.marker_count; ++i) {
+ const PhAreaMarker *marker = &world->area.markers[i];
+ const PhMarkerDef *def = ph_game_marker_def(world->area_id,
+ marker->symbol);
+
+ if (!def || def->kind != PH_MARKER_PORTAL) continue;
+ player->pos = (PhVec2){ PH_TILE_CENTER(marker->tile_x),
+ PH_TILE_CENTER(marker->tile_y) };
+ return ph_game_update_portal(world);
+ }
+ return -1;
+}
+
+static int
ph_area_shop_test(void)
{
PhWorld world;
@@ -232,25 +251,30 @@ ph_area_shop_test(void)
const PhItemDef *ether;
int potion_before;
int slime_found = 0;
+ int shop_id = -1;
int i;
if (ph_game_world_init(&world, 320.0f, 240.0f) < 0) return 1;
player = &world.entities[world.player_index];
potion = ph_world_item_def(&world, PH_ITEM_HEALING_POTION);
ether = ph_world_item_def(&world, PH_ITEM_ETHER);
- if (!potion || !ether || world.area_id != PH_AREA_MEADOW || player->gold != 40)
+ if (!potion || !ether || world.area_id != PH_AREA_MEADOW || player->gold != 40 ||
+ world.initialized_area_count != 1)
goto fail;
- player->pos = (PhVec2){ PH_TILE_CENTER(29), PH_TILE_CENTER(2) };
- if (ph_game_update_portal(&world) != 1 ||
- world.area_id != PH_AREA_STONEHOLLOW) goto fail;
+ if (ph_enter_portal(&world) != 1 ||
+ world.area_id != PH_AREA_STONEHOLLOW ||
+ world.initialized_area_count != 2) goto fail;
if (!ph_area_tile_blocked(&world.area, 3, 2) ||
ph_area_tile_blocked(&world.area, 4, 3) ||
ph_area_tile_blocked(&world.area, 6, 6)) goto fail;
for (i = 0; i < world.entity_count; ++i) {
const PhEntityDef *def = ph_world_entity_def(&world,
world.entities[i].type_id);
+ const PhInteractionDef *interaction = def ?
+ ph_game_interaction_def(def->interaction_id) : NULL;
- if (def && def->interaction_id == PH_SHOP_STONEHOLLOW) {
+ if (interaction && interaction->kind == PH_INTERACTION_SHOP) {
+ shop_id = interaction->content_id;
player->pos = (PhVec2){ world.entities[i].pos.x,
world.entities[i].pos.y + PH_TILE_SIZE };
player->facing_x = 0;
@@ -265,7 +289,7 @@ ph_area_shop_test(void)
ph_game_interaction_def(world.interaction_id);
if (!interaction || interaction->kind != PH_INTERACTION_SHOP ||
- interaction->content_id != PH_SHOP_STONEHOLLOW) goto fail;
+ interaction->content_id != shop_id) goto fail;
}
potion_before = ph_world_item_amount(&world, potion->id);
if (ph_world_buy_item(&world, world.player_index, potion->id) < 0 ||
@@ -277,8 +301,8 @@ ph_area_shop_test(void)
player->mp = 0;
if (ph_world_use_inventory_item(&world, world.player_index, ether->id) < 0 ||
player->mp != ether->use_power) goto fail;
- player->pos = (PhVec2){ PH_TILE_CENTER(15), PH_TILE_CENTER(22) };
- if (ph_game_update_portal(&world) != 1 || world.area_id != PH_AREA_MEADOW)
+ if (ph_enter_portal(&world) != 1 || world.area_id != PH_AREA_MEADOW ||
+ world.initialized_area_count != 2)
goto fail;
for (i = 0; i < world.entity_count; ++i) {
const PhEntityDef *def = ph_world_entity_def(&world,
diff --git a/src/tools/mapc.c b/src/tools/mapc.c
@@ -18,9 +18,16 @@ typedef struct {
uint32_t version;
uint32_t width;
uint32_t height;
+ uint32_t marker_count;
char name[PH_AREA_NAME_MAX];
} PhMapHeader;
+typedef struct {
+ uint32_t symbol;
+ uint32_t tile_x;
+ uint32_t tile_y;
+} PhMapMarker;
+
static int
ph_tile_index(unsigned char symbol)
{
@@ -31,6 +38,27 @@ ph_tile_index(unsigned char symbol)
return -1;
}
+static const PhMarkerDef *
+ph_marker_def(int area_id, unsigned char symbol)
+{
+ size_t i;
+
+ for (i = 0; i < LEN(ph_marker_defs); ++i)
+ if (ph_marker_defs[i].area_id == area_id &&
+ ph_marker_defs[i].symbol == symbol) return &ph_marker_defs[i];
+ return NULL;
+}
+
+static int
+ph_area_id(const char *path)
+{
+ size_t i;
+
+ for (i = 0; i < LEN(ph_area_defs); ++i)
+ if (strcmp(ph_area_defs[i].path, path) == 0) return ph_area_defs[i].id;
+ return 0;
+}
+
static void
ph_strip_newline(char *line)
{
@@ -52,9 +80,17 @@ ph_compile_map(const char *src_path, const char *dst_path)
};
char line[PH_MAP_LINE_MAX];
unsigned char *tiles = NULL;
+ PhMapMarker *markers = NULL;
size_t tile_cap = 0;
size_t tile_count = 0;
+ size_t marker_cap = 0;
int in_tiles = 0;
+ int area_id = ph_area_id(dst_path);
+
+ if (!area_id) {
+ fprintf(stderr, "%s: output path has no area definition\n", dst_path);
+ return 1;
+ }
if (LEN(ph_tile_defs) > 256) {
fprintf(stderr, "too many configured tile types\n");
@@ -82,6 +118,7 @@ ph_compile_map(const char *src_path, const char *dst_path)
name_len = strlen(name);
if (name_len >= sizeof(header.name)) {
fprintf(stderr, "%s: map name too long\n", src_path);
+ free(markers);
free(tiles);
fclose(src);
return 1;
@@ -99,15 +136,37 @@ 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]);
+ const PhMarkerDef *marker = ph_marker_def(area_id,
+ (unsigned char)line[x]);
+ int tile = ph_tile_index(marker ? marker->tile_symbol :
+ (unsigned char)line[x]);
if (tile < 0) {
- fprintf(stderr, "%s: unknown tile '%c' in row %u\n",
+ fprintf(stderr, "%s: unknown tile or marker '%c' in row %u\n",
src_path, line[x], header.height + 1);
+ free(markers);
free(tiles);
fclose(src);
return 1;
}
+ if (marker) {
+ PhMapMarker *new_markers;
+
+ if (header.marker_count == marker_cap) {
+ marker_cap = marker_cap ? marker_cap * 2 : 16;
+ new_markers = realloc(markers,
+ marker_cap * sizeof(*markers));
+ if (!new_markers) {
+ free(markers);
+ free(tiles);
+ fclose(src);
+ return 1;
+ }
+ markers = new_markers;
+ }
+ markers[header.marker_count++] = (PhMapMarker){
+ (unsigned char)line[x], (uint32_t)x, header.height };
+ }
line[x] = (char)tile;
}
if (header.width == 0) {
@@ -115,6 +174,7 @@ ph_compile_map(const char *src_path, const char *dst_path)
} else if (len != header.width) {
fprintf(stderr, "%s: inconsistent row width: got %zu, expected %u\n",
src_path, len, header.width);
+ free(markers);
free(tiles);
fclose(src);
return 1;
@@ -129,6 +189,7 @@ ph_compile_map(const char *src_path, const char *dst_path)
}
new_tiles = realloc(tiles, new_cap);
if (!new_tiles) {
+ free(markers);
free(tiles);
fclose(src);
return 1;
@@ -145,6 +206,7 @@ ph_compile_map(const char *src_path, const char *dst_path)
if (header.width == 0 || header.height == 0 || header.name[0] == '\0') {
fprintf(stderr, "%s: missing name or tile data\n", src_path);
+ free(markers);
free(tiles);
return 1;
}
@@ -152,18 +214,23 @@ ph_compile_map(const char *src_path, const char *dst_path)
dst = fopen(dst_path, "wb");
if (!dst) {
perror(dst_path);
+ free(markers);
free(tiles);
return 1;
}
if (fwrite(&header, sizeof(header), 1, dst) != 1 ||
- fwrite(tiles, 1, tile_count, dst) != tile_count) {
+ fwrite(tiles, 1, tile_count, dst) != tile_count ||
+ fwrite(markers, sizeof(*markers), header.marker_count, dst) !=
+ header.marker_count) {
perror(dst_path);
+ free(markers);
free(tiles);
fclose(dst);
return 1;
}
+ free(markers);
free(tiles);
fclose(dst);
return 0;