phantasia

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

commit ae5a1592ddbf20295d9034c62d7a1be9bf2ef2bc
parent 76ff3492aea3b0d0ef01f68be4b6eec5ab1e0d1a
Author: beep <beep@wimdupont.com>
Date:   Thu, 30 Jul 2026 09:39:30 +0000

Add semantic autotiling and shop interior

Diffstat:
Mdata/maps/ashen-meadow.txt | 48++++++++++++++++++++++++------------------------
Adata/maps/mira-shop.txt | 17+++++++++++++++++
Mdata/maps/stonehollow.txt | 48++++++++++++++++++++++++------------------------
Msrc/engine/render.c | 21+++++++++++++++++++--
Msrc/engine/world.h | 6++++++
Msrc/game/content.c | 45+++++++++++++++++++++++++++++----------------
Msrc/game/content.h | 7++++---
Msrc/tests/render.c | 17+++++++++++++++++
Msrc/tests/smoke.c | 22+++++++++++++++-------
9 files changed, 155 insertions(+), 76 deletions(-)

diff --git a/data/maps/ashen-meadow.txt b/data/maps/ashen-meadow.txt @@ -1,26 +1,26 @@ name: Ashen Meadow -ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBC -D:..;...:.....;..:.....;.......F -D..:....;..............:....e>.F -D.....ABC....;......:..........F -D....@1EF........:.......;.....F -D.....2HI...:........ABC.......F -D..;.......a.........DEF..:....F -D........:...........GHI.......F -D....;.........:..........;....F -D.........ABC................:.F -D..:......DEF...;..............F -D.........GHI..........:.......F -D....;.............ABC.........F -D........:.........DEF.....;...F -D..ABC.............GHI.........F -D..DEF...;....:...............:F -D..GHI..............;..........F -D...........ABC..........:.....F -D...:.......DEF....;...........F -D...........GHI...........;....F -D..;.............:.............F -D.......:..............;.......F -D..;.........:..............;..F -GHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHI +################################ +#:..;...:.....;..:.....;.......# +#..:....;..............:....e>.# +#............;......:..........# +#....@1..........:.......;.....# +#.....2.....:........###.......# +#..;.......a.........###..:....# +#........:...........###.......# +#....;.........:..........;....# +#.........###................:.# +#..:......###...;..............# +#.........###..........:.......# +#....;.............###.........# +#........:.........###.....;...# +#..###.............###.........# +#..###...;....:...............:# +#..###..............;..........# +#...........###..........:.....# +#...:.......###....;...........# +#...........###...........;....# +#..;.............:.............# +#.......:..............;.......# +#..;.........:..............;..# +################################ diff --git a/data/maps/mira-shop.txt b/data/maps/mira-shop.txt @@ -0,0 +1,17 @@ +name: Mira's Sundries + +[^^^^^^^^^^^^^^^^^^] +{==================} +{==================} +{==================} +{========m=========} +{==================} +{==================} +{==================} +{==================} +{==================} +{==================} +{==================} +{==================} +{========e=========} +(_________<________) diff --git a/data/maps/stonehollow.txt b/data/maps/stonehollow.txt @@ -1,26 +1,26 @@ name: Stonehollow -ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBC -D..............................F -D..J....K....L....M....N....O..F -D..m...........................F -D..,,,,,,,,,,,,,,,,,,,,,,,,,,..F -D..,....:.........;.........,..F -D..,........................,..F -D..,....P.....Q.....R.......,..F -D..,........................,..F -D..,.........ABC............,..F -D..,,,,,,,,,,DEF,,,,,,,,,,,,,..F -D..,.........GHI............,..F -D..,....;...............:...,..F -D..,........................,..F -D..,......M......K......P...,..F -D..,,,,,,,,,,,,,,,,,,,,,,,,,,..F -D.............,................F -D....;........,......:.........F -D.............,................F -D..:..........,..........;.....F -D.............,................F -D.............e................F -D.............<................F -GHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHI +################################ +#..............................# +#..>....K....L....M....N....O..# +#..i...........................# +#..,,,,,,,,,,,,,,,,,,,,,,,,,,..# +#..,....:.........;.........,..# +#..,........................,..# +#..,....P.....Q.....R.......,..# +#..,........................,..# +#..,.........###............,..# +#..,,,,,,,,,,###,,,,,,,,,,,,,..# +#..,.........###............,..# +#..,....;...............:...,..# +#..,........................,..# +#..,......M......K......P...,..# +#..,,,,,,,,,,,,,,,,,,,,,,,,,,..# +#.............,................# +#....;........,......:.........# +#.............,................# +#..:..........,..........;.....# +#.............,................# +#.............e................# +#.............<................# +################################ diff --git a/src/engine/render.c b/src/engine/render.c @@ -123,6 +123,8 @@ ph_draw_area(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld .h = (float)PH_TILE_SIZE, }; SDL_FRect src; + int sprite_tile_x; + int sprite_tile_y; if (!def) continue; if (def->background_asset_id) { @@ -140,9 +142,24 @@ ph_draw_area(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld } texture = ph_render_asset(assets, def->asset_id); if (!texture) continue; + sprite_tile_x = def->sprite_tile_x; + sprite_tile_y = def->sprite_tile_y; + if (def->autotile == PH_TILE_AUTOTILE_3X3) { + const PhTileDef *north = ph_area_tile_def(&world->area, tx, ty - 1); + const PhTileDef *east = ph_area_tile_def(&world->area, tx + 1, ty); + const PhTileDef *south = ph_area_tile_def(&world->area, tx, ty + 1); + const PhTileDef *west = ph_area_tile_def(&world->area, tx - 1, ty); + int joins_north = north && north->symbol == def->symbol; + int joins_east = east && east->symbol == def->symbol; + int joins_south = south && south->symbol == def->symbol; + int joins_west = west && west->symbol == def->symbol; + + sprite_tile_x += joins_west == joins_east ? 1 : joins_west ? 2 : 0; + sprite_tile_y += joins_north == joins_south ? 1 : joins_north ? 2 : 0; + } src = (SDL_FRect){ - .x = (float)(def->sprite_tile_x * PH_TILE_SIZE), - .y = (float)(def->sprite_tile_y * PH_TILE_SIZE), + .x = (float)(sprite_tile_x * PH_TILE_SIZE), + .y = (float)(sprite_tile_y * PH_TILE_SIZE), .w = (float)PH_TILE_SIZE, .h = (float)PH_TILE_SIZE, }; diff --git a/src/engine/world.h b/src/engine/world.h @@ -112,8 +112,14 @@ typedef struct { int background_asset_id; int background_sprite_tile_x; int background_sprite_tile_y; + int autotile; } PhTileDef; +enum { + PH_TILE_AUTOTILE_NONE, + PH_TILE_AUTOTILE_3X3, +}; + typedef struct { int id; const char *name; diff --git a/src/game/content.c b/src/game/content.c @@ -10,23 +10,16 @@ const PhAssetDef ph_asset_defs[PH_ASSET_COUNT] = { const PhAreaDef ph_area_defs[PH_AREA_COUNT] = { { PH_AREA_MEADOW, "obj/maps/ashen-meadow.phmap" }, { PH_AREA_STONEHOLLOW, "obj/maps/stonehollow.phmap" }, + { PH_AREA_MIRA_SHOP, "obj/maps/mira-shop.phmap" }, }; const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = { - { '.', 0, PH_ASSET_TILESET, 1, 1, 0, 0, 0 }, - { ':', 0, PH_ASSET_TILESET, 1, 0, 0, 0, 0 }, - { ';', 0, PH_ASSET_TILESET, 2, 0, 0, 0, 0 }, - { ',', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0 }, - { 'T', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0 }, - { 'A', 1, PH_ASSET_TILESET, 9, 4, 0, 0, 0 }, - { 'B', 1, PH_ASSET_TILESET, 10, 4, 0, 0, 0 }, - { 'C', 1, PH_ASSET_TILESET, 11, 4, 0, 0, 0 }, - { 'D', 1, PH_ASSET_TILESET, 9, 5, 0, 0, 0 }, - { 'E', 1, PH_ASSET_TILESET, 10, 5, 0, 0, 0 }, - { 'F', 1, PH_ASSET_TILESET, 11, 5, 0, 0, 0 }, - { 'G', 1, PH_ASSET_TILESET, 9, 6, 0, 0, 0 }, - { 'H', 1, PH_ASSET_TILESET, 10, 6, 0, 0, 0 }, - { 'I', 1, PH_ASSET_TILESET, 11, 6, 0, 0, 0 }, + { '.', 0, PH_ASSET_TILESET, 1, 1, 0, 0, 0, 0 }, + { ':', 0, PH_ASSET_TILESET, 1, 0, 0, 0, 0, 0 }, + { ';', 0, PH_ASSET_TILESET, 2, 0, 0, 0, 0, 0 }, + { ',', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0, 0 }, + { 'T', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0, 0 }, + { '#', 1, PH_ASSET_TILESET, 9, 4, 0, 0, 0, PH_TILE_AUTOTILE_3X3 }, { .symbol = 'J', .blocks_movement = 1, .asset_id = PH_ASSET_TILESET, .sprite_tile_x = 0, .sprite_tile_y = 16, .background_asset_id = PH_ASSET_TILESET, @@ -63,6 +56,20 @@ const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = { .sprite_tile_x = 3, .sprite_tile_y = 18, .background_asset_id = PH_ASSET_TILESET, .background_sprite_tile_x = 1, .background_sprite_tile_y = 1 }, + { .symbol = 'j', .asset_id = PH_ASSET_TILESET, + .sprite_tile_x = 0, .sprite_tile_y = 16, + .background_asset_id = PH_ASSET_TILESET, + .background_sprite_tile_x = 1, .background_sprite_tile_y = 1 }, + { '[', 1, PH_ASSET_TILESET, 9, 1, 0, 0, 0, 0 }, + { '^', 1, PH_ASSET_TILESET, 10, 1, 0, 0, 0, 0 }, + { ']', 1, PH_ASSET_TILESET, 11, 1, 0, 0, 0, 0 }, + { '{', 1, PH_ASSET_TILESET, 9, 2, 0, 0, 0, 0 }, + { '=', 0, PH_ASSET_TILESET, 10, 2, 0, 0, 0, 0 }, + { '}', 1, PH_ASSET_TILESET, 11, 2, 0, 0, 0, 0 }, + { '(', 1, PH_ASSET_TILESET, 9, 3, 0, 0, 0, 0 }, + { '_', 1, PH_ASSET_TILESET, 10, 3, 0, 0, 0, 0 }, + { ')', 1, PH_ASSET_TILESET, 11, 3, 0, 0, 0, 0 }, + { '+', 0, PH_ASSET_TILESET, 10, 3, 0, 0, 0, 0 }, }; const PhSpellDef ph_spell_defs[PH_SPELL_DEF_COUNT] = { @@ -212,11 +219,17 @@ const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT] = { { PH_AREA_MEADOW, '>', 'T', 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, '>', 'j', PH_MARKER_PORTAL, + .destination_area_id = PH_AREA_MIRA_SHOP, .destination_symbol = 'e' }, { PH_AREA_STONEHOLLOW, '<', 'T', PH_MARKER_PORTAL, .destination_area_id = PH_AREA_MEADOW, .destination_symbol = 'e' }, { PH_AREA_STONEHOLLOW, 'e', ',', PH_MARKER_ARRIVAL, .content_id = 0 }, + { PH_AREA_STONEHOLLOW, 'i', ',', PH_MARKER_ARRIVAL, .content_id = 0 }, + { PH_AREA_MIRA_SHOP, 'm', '=', PH_MARKER_ENTITY, + PH_DEF_SHOPKEEPER, .amount = 0 }, + { PH_AREA_MIRA_SHOP, 'e', '=', PH_MARKER_ARRIVAL, .content_id = 0 }, + { PH_AREA_MIRA_SHOP, '<', '+', PH_MARKER_PORTAL, + .destination_area_id = PH_AREA_STONEHOLLOW, .destination_symbol = 'i' }, }; static const PhAreaCatalog ph_area_catalog = { diff --git a/src/game/content.h b/src/game/content.h @@ -55,7 +55,8 @@ enum { enum { PH_AREA_MEADOW = 1, PH_AREA_STONEHOLLOW, - PH_AREA_COUNT = 2, + PH_AREA_MIRA_SHOP, + PH_AREA_COUNT = 3, PH_START_AREA = PH_AREA_MEADOW, }; @@ -73,8 +74,8 @@ enum { }; enum { - PH_TILE_DEF_COUNT = 23, - PH_MARKER_DEF_COUNT = 9, + PH_TILE_DEF_COUNT = 26, + PH_MARKER_DEF_COUNT = 13, }; extern const PhAssetDef ph_asset_defs[PH_ASSET_COUNT]; diff --git a/src/tests/render.c b/src/tests/render.c @@ -241,6 +241,23 @@ main(void) fprintf(stderr, "render smoke test failed: town portal\n"); result = 1; } + for (i = 0; i < world.area.marker_count; ++i) { + const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, + world.area_id, world.area.markers[i].symbol); + + if (!def || def->kind != PH_MARKER_PORTAL || + def->destination_area_id != PH_AREA_MIRA_SHOP) 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_world_update_portal(&world, ph_game_world.areas) != 1 || + world.area_id != PH_AREA_MIRA_SHOP) { + fprintf(stderr, "render smoke test failed: shop portal\n"); + result = 1; + } ph_shop_open(&shop_ui, ph_shop_defs[0].id); shop_ui.mode = PH_SHOP_TALK; context.menu = PH_MENU_SHOP; diff --git a/src/tests/smoke.c b/src/tests/smoke.c @@ -298,7 +298,7 @@ ph_logic_test(int open, int blocked) } static int -ph_enter_portal(PhWorld *world) +ph_enter_portal(PhWorld *world, int destination_area_id) { PhEntity *player = &world->entities[world->player_index]; int i; @@ -309,9 +309,10 @@ ph_enter_portal(PhWorld *world) world->area_id, marker->symbol); const PhTileDef *tile; - if (!def || def->kind != PH_MARKER_PORTAL) continue; + if (!def || def->kind != PH_MARKER_PORTAL || + def->destination_area_id != destination_area_id) continue; tile = ph_area_tile_def(&world->area, marker->tile_x, marker->tile_y); - if (!tile || tile->symbol != 'T') return -1; + if (!tile || tile->blocks_movement) return -1; player->pos = (PhVec2){ PH_TILE_CENTER(marker->tile_x), PH_TILE_CENTER(marker->tile_y) }; return ph_world_update_portal(world, ph_game_world.areas); @@ -338,12 +339,16 @@ ph_area_shop_test(void) if (!potion || !ether || world.area_id != PH_AREA_MEADOW || player->gold != 40 || world.initialized_area_count != 1) goto fail; - if (ph_enter_portal(&world) != 1 || + if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 || world.area_id != PH_AREA_STONEHOLLOW || world.initialized_area_count != 2) goto fail; - if (!ph_area_tile_blocked(&world.area, 3, 2) || + if (ph_area_tile_blocked(&world.area, 3, 2) || + !ph_area_tile_blocked(&world.area, 8, 2) || ph_area_tile_blocked(&world.area, 4, 4) || ph_area_tile_blocked(&world.area, 6, 6)) goto fail; + if (ph_enter_portal(&world, PH_AREA_MIRA_SHOP) != 1 || + world.area_id != PH_AREA_MIRA_SHOP || + world.initialized_area_count != 3) goto fail; for (i = 0; i < world.entity_count; ++i) { const PhEntityDef *def = ph_world_entity_def(&world, world.entities[i].type_id); @@ -378,8 +383,11 @@ ph_area_shop_test(void) player->vitals.mp = 0; if (ph_world_use_inventory_item(&world, world.player_index, ether->id) < 0 || player->vitals.mp != ether->use_power) goto fail; - if (ph_enter_portal(&world) != 1 || world.area_id != PH_AREA_MEADOW || - world.initialized_area_count != 2) + if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 || + world.area_id != PH_AREA_STONEHOLLOW || + world.initialized_area_count != 3 || + ph_enter_portal(&world, PH_AREA_MEADOW) != 1 || + world.area_id != PH_AREA_MEADOW || world.initialized_area_count != 3) goto fail; for (i = 0; i < world.entity_count; ++i) { const PhEntityDef *def = ph_world_entity_def(&world,