phantasia

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

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:
MMakefile | 2+-
MREADME.adoc | 54+++++++++++++++++++++++++++++++++++++++---------------
Mdata/maps/ashen-meadow.txt | 8++++----
Mdata/maps/stonehollow.txt | 6+++---
Msrc/engine/world.c | 44+++++++++++++++++++++++++++++++++++++++++++-
Msrc/engine/world.h | 13++++++++++++-
Msrc/game/area.c | 115++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/game/area.h | 1+
Msrc/game/content.c | 36++++++++++++++++--------------------
Msrc/game/content.h | 41+++++++++++++++++------------------------
Msrc/game/main.c | 36++++++++++++++++++++++++++++--------
Msrc/tests/smoke.c | 40++++++++++++++++++++++++++++++++--------
Msrc/tools/mapc.c | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
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;