phantasia

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

commit 5485df2448c3d97cdbbbc6fa49169d20cced16fb
parent ae151fed5672c21a4e3f470304b661c8b6f30012
Author: beep <beep@wimdupont.com>
Date:   Thu, 30 Jul 2026 10:09:22 +0000

Add reusable magick shop system

Diffstat:
Adata/maps/magick-shop.txt | 17+++++++++++++++++
Mdata/maps/stonehollow.txt | 4++--
Msrc/engine/battle.c | 52+++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/engine/battle.h | 10++++++++--
Msrc/game/content.c | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/game/content.h | 38+++++++++++++++++++++++++++-----------
Msrc/game/main.c | 15+++++++++++----
Msrc/game/presentation.c | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/game/presentation.h | 4+++-
Msrc/tests/render.c | 30++++++++++++++++++++++++++----
Msrc/tests/smoke.c | 51+++++++++++++++++++++++++++++++++++++++++++++++----
11 files changed, 312 insertions(+), 64 deletions(-)

diff --git a/data/maps/magick-shop.txt b/data/maps/magick-shop.txt @@ -0,0 +1,17 @@ +name: Orin's Magick + +[^^^^^^^^^^^^^^^^^^] +{==================} +{==================} +{==================} +{========n=========} +{==================} +{==================} +{==================} +{==================} +{==================} +{==================} +{==================} +{==================} +{=========e========} +(_________<________) diff --git a/data/maps/stonehollow.txt b/data/maps/stonehollow.txt @@ -2,8 +2,8 @@ name: Stonehollow ################################ #..............................# -#..>....K....L....M....N....O..# -#..i...........................# +#..>....*....L....M....N....O..# +#..i....o......................# #..,,,,,,,,,,,,,,,,,,,,,,,,,,..# #..,....:.........;.........,..# #..,........................,..# diff --git a/src/engine/battle.c b/src/engine/battle.c @@ -73,16 +73,62 @@ ph_battle_item(const PhWorld *world) } const PhSpellDef * -ph_spell_def(const PhBattle *battle, int id) +ph_spellbook_def(const PhSpellBook *spells, int id) { int i; - for (i = 0; i < battle->spells.def_count; ++i) - if (battle->spells.defs[i].id == id) return &battle->spells.defs[i]; + if (!spells || spells->def_count < 0 || + (spells->def_count > 0 && !spells->defs)) return NULL; + for (i = 0; i < spells->def_count; ++i) + if (spells->defs[i].id == id) return &spells->defs[i]; return NULL; } const PhSpellDef * +ph_spell_def(const PhBattle *battle, int id) +{ + return ph_spellbook_def(&battle->spells, id); +} + +int +ph_spellbook_has(const PhSpellBook *spells, int id) +{ + int i; + + if (!spells || spells->learned_count < 0 || + spells->learned_count > PH_SPELL_MAX) return 0; + for (i = 0; i < spells->learned_count; ++i) + if (spells->learned[i] == id) return 1; + return 0; +} + +int +ph_spellbook_learn(PhSpellBook *spells, int id) +{ + if (!spells || !ph_spellbook_def(spells, id) || ph_spellbook_has(spells, id) || + spells->learned_count < 0 || spells->learned_count >= PH_SPELL_MAX) + return -1; + spells->learned[spells->learned_count++] = id; + return 0; +} + +int +ph_spellbook_buy(PhSpellBook *spells, PhWorld *world, int entity_index, + int spell_id) +{ + const PhSpellDef *spell = ph_spellbook_def(spells, spell_id); + PhEntity *entity; + + if (!spell || !world || entity_index < 0 || entity_index >= world->entity_count || + ph_spellbook_has(spells, spell_id)) return -1; + entity = &world->entities[entity_index]; + if (!entity->active || spell->buy_value < 0 || entity->gold < spell->buy_value || + ph_spellbook_learn(spells, spell_id) < 0) return -1; + entity->gold -= spell->buy_value; + return 0; +} + +const PhSpellDef * ph_battle_spell(const PhBattle *battle) { if (battle->choice.spell < 0 || battle->choice.spell >= battle->spells.learned_count) diff --git a/src/engine/battle.h b/src/engine/battle.h @@ -3,7 +3,7 @@ #include "engine/world.h" -enum { PH_BATTLE_ORDER_COUNT = 6 }; +enum { PH_BATTLE_ORDER_COUNT = 6, PH_SPELL_MAX = 16 }; typedef enum { PH_BATTLE_COMMAND, @@ -32,12 +32,13 @@ typedef struct { int mp_cost; int power; int delay_percent; + int buy_value; PhTimedEffect accuracy; } PhSpellDef; typedef struct { const PhSpellDef *defs; - const int *learned; + int learned[PH_SPELL_MAX]; int def_count; int learned_count; } PhSpellBook; @@ -77,6 +78,11 @@ PhBattleResult ph_battle_update(PhBattle *battle, PhWorld *world, float dt, void ph_battle_order(const PhBattle *battle, const PhWorld *world, int order[PH_BATTLE_ORDER_COUNT]); const PhSpellDef *ph_spell_def(const PhBattle *battle, int id); +const PhSpellDef *ph_spellbook_def(const PhSpellBook *spells, int id); +int ph_spellbook_has(const PhSpellBook *spells, int id); +int ph_spellbook_learn(PhSpellBook *spells, int id); +int ph_spellbook_buy(PhSpellBook *spells, PhWorld *world, int entity_index, + int spell_id); const PhSpellDef *ph_battle_spell(const PhBattle *battle); float ph_battle_lunge(const PhBattle *battle, PhBattlePhase phase); diff --git a/src/game/content.c b/src/game/content.c @@ -11,6 +11,7 @@ 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" }, + { PH_AREA_MAGICK_SHOP, "obj/maps/magick-shop.phmap" }, }; const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = { @@ -70,27 +71,38 @@ const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = { .sprite_tile_x = 0, .sprite_tile_y = 16, .background_asset_id = PH_ASSET_TILESET, .background_sprite_tile_x = 1, .background_sprite_tile_y = 1 }, + { .symbol = 'k', .asset_id = PH_ASSET_TILESET, + .sprite_tile_x = 1, .sprite_tile_y = 16, + .background_asset_id = PH_ASSET_TILESET, + .background_sprite_tile_x = 1, .background_sprite_tile_y = 1 }, { '[', 0, PH_ASSET_TILESET, 9, 1, 0, 0, 0, 0, 0 }, { '^', 0, PH_ASSET_TILESET, 10, 1, 0, 0, 0, 0, 0 }, { ']', 0, PH_ASSET_TILESET, 11, 1, 0, 0, 0, 0, 0 }, { '{', 0, PH_ASSET_TILESET, 9, 2, 0, 0, 0, 0, 0 }, { '=', 0, PH_ASSET_TILESET, 10, 2, 0, 0, 0, 0, 0 }, { '}', 0, PH_ASSET_TILESET, 11, 2, 0, 0, 0, 0, 0 }, - { '(', 0, PH_ASSET_TILESET, 9, 3, 0, 0, 0, 0, 0 }, - { '_', 0, PH_ASSET_TILESET, 10, 3, 0, 0, 0, 0, 0 }, - { ')', 0, PH_ASSET_TILESET, 11, 3, 0, 0, 0, 0, 0 }, + { '(', 1, PH_ASSET_TILESET, 9, 3, 0, 0, 0, 0, 0 }, + { '_', 1, PH_ASSET_TILESET, 10, 3, 0, 0, 0, 0, 0 }, + { ')', 1, PH_ASSET_TILESET, 11, 3, 0, 0, 0, 0, 0 }, { '+', 0, PH_ASSET_TILESET, 5, 15, 0, 0, 0, 0, 0 }, }; const PhSpellDef ph_spell_defs[PH_SPELL_DEF_COUNT] = { - { PH_SPELL_FLASH, "Flash", 15, 5, 50, { 20, 3 } }, + { .id = PH_SPELL_FLASH, .name = "Flash", .mp_cost = 15, .power = 5, + .delay_percent = 50, .accuracy = { 20, 3 } }, + { .id = PH_SPELL_EMBER, .name = "Ember", .mp_cost = 8, .power = 7, + .delay_percent = 100, .buy_value = 24 }, + { .id = PH_SPELL_DAZE, .name = "Daze", .mp_cost = 12, .power = 4, + .delay_percent = 85, .buy_value = 48, .accuracy = { 15, 2 } }, + { .id = PH_SPELL_INFERNO, .name = "Inferno", .mp_cost = 28, .power = 24, + .delay_percent = 140, .buy_value = 600 }, + { .id = PH_SPELL_METEOR, .name = "Meteor", .mp_cost = 45, .power = 40, + .delay_percent = 180, .buy_value = 2400 }, }; -const int ph_player_spells[PH_PLAYER_SPELL_COUNT] = { PH_SPELL_FLASH }; - const PhSpellBook ph_player_spellbook = { .defs = ph_spell_defs, - .learned = ph_player_spells, + .learned = { PH_SPELL_FLASH }, .def_count = PH_SPELL_DEF_COUNT, .learned_count = PH_PLAYER_SPELL_COUNT, }; @@ -148,6 +160,20 @@ const PhEntityDef ph_entity_defs[PH_ENTITY_DEF_COUNT] = { .blocks_movement = 1, .kind = PH_ENTITY_NPC, }, + { + .id = PH_DEF_MAGICK_KEEPER, + .name = "Orin", + .interaction_id = PH_INTERACTION_MAGUS, + .asset_id = PH_ASSET_HERO, + .sprite_tiles = { + [PH_SPRITE_DOWN] = { { 0, 0 } }, + [PH_SPRITE_UP] = { { 0, 0 } }, + [PH_SPRITE_SIDE] = { { 0, 0 } }, + }, + .animation_frames = 1, + .blocks_movement = 1, + .kind = PH_ENTITY_NPC, + }, }; const PhItemDef ph_item_defs[PH_ITEM_DEF_COUNT] = { @@ -235,11 +261,19 @@ const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT] = { .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_STONEHOLLOW, '*', 'k', PH_MARKER_PORTAL, + .destination_area_id = PH_AREA_MAGICK_SHOP, .destination_symbol = 'e' }, + { PH_AREA_STONEHOLLOW, 'o', ',', 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' }, + { PH_AREA_MAGICK_SHOP, 'n', '=', PH_MARKER_ENTITY, + PH_DEF_MAGICK_KEEPER, .amount = 0 }, + { PH_AREA_MAGICK_SHOP, 'e', '=', PH_MARKER_ARRIVAL, .content_id = 0 }, + { PH_AREA_MAGICK_SHOP, '<', '+', PH_MARKER_PORTAL, + .destination_area_id = PH_AREA_STONEHOLLOW, .destination_symbol = 'o' }, }; static const PhAreaCatalog ph_area_catalog = { @@ -268,17 +302,31 @@ const PhShopDef ph_shop_defs[PH_SHOP_COUNT] = { { .id = PH_SHOP_STONEHOLLOW, .name = "Mira's Sundries", - .items = { PH_ITEM_HEALING_POTION, PH_ITEM_ETHER }, - .item_count = 2, + .kind = PH_SHOP_ITEMS, + .stock = { PH_ITEM_HEALING_POTION, PH_ITEM_ETHER }, + .stock_count = 2, .talk = { "Fine weather for adventuring.", "Terrible weather for staying solvent.", }, }, + { + .id = PH_SHOP_MAGICK, + .name = "Orin's Magick", + .kind = PH_SHOP_SPELLS, + .stock = { PH_SPELL_EMBER, PH_SPELL_DAZE, + PH_SPELL_INFERNO, PH_SPELL_METEOR }, + .stock_count = 4, + .talk = { + "A spell is a promise made to fire.", + "The costly promises answer last.", + }, + }, }; const PhInteractionDef ph_interaction_defs[PH_INTERACTION_COUNT] = { { PH_INTERACTION_MIRA, PH_INTERACTION_SHOP, PH_SHOP_STONEHOLLOW }, + { PH_INTERACTION_MAGUS, PH_INTERACTION_SHOP, PH_SHOP_MAGICK }, }; const PhShopDef * diff --git a/src/game/content.h b/src/game/content.h @@ -6,13 +6,19 @@ #include "engine/render.h" #include "engine/world.h" -enum { PH_SHOP_ITEM_MAX = 8, PH_SHOP_TALK_LINES = 2 }; +enum { PH_SHOP_STOCK_MAX = 8, PH_SHOP_TALK_LINES = 2 }; + +typedef enum { + PH_SHOP_ITEMS, + PH_SHOP_SPELLS, +} PhShopKind; typedef struct { int id; const char *name; - int items[PH_SHOP_ITEM_MAX]; - int item_count; + PhShopKind kind; + int stock[PH_SHOP_STOCK_MAX]; + int stock_count; const char *talk[PH_SHOP_TALK_LINES]; } PhShopDef; @@ -40,7 +46,8 @@ enum { PH_DEF_PLAYER = 1, PH_DEF_SLIME, PH_DEF_SHOPKEEPER, - PH_ENTITY_DEF_COUNT = 3, + PH_DEF_MAGICK_KEEPER, + PH_ENTITY_DEF_COUNT = 4, }; enum { @@ -56,26 +63,36 @@ enum { PH_AREA_MEADOW = 1, PH_AREA_STONEHOLLOW, PH_AREA_MIRA_SHOP, - PH_AREA_COUNT = 3, + PH_AREA_MAGICK_SHOP, + PH_AREA_COUNT = 4, PH_START_AREA = PH_AREA_MEADOW, }; -enum { PH_SHOP_STONEHOLLOW = 1, PH_SHOP_COUNT = 1 }; +enum { + PH_SHOP_STONEHOLLOW = 1, + PH_SHOP_MAGICK, + PH_SHOP_COUNT = 2, +}; enum { PH_INTERACTION_MIRA = 1, - PH_INTERACTION_COUNT = 1, + PH_INTERACTION_MAGUS, + PH_INTERACTION_COUNT = 2, }; enum { PH_SPELL_FLASH = 1, - PH_SPELL_DEF_COUNT = 1, + PH_SPELL_EMBER, + PH_SPELL_DAZE, + PH_SPELL_INFERNO, + PH_SPELL_METEOR, + PH_SPELL_DEF_COUNT = 5, PH_PLAYER_SPELL_COUNT = 1, }; enum { - PH_TILE_DEF_COUNT = 26, - PH_MARKER_DEF_COUNT = 13, + PH_TILE_DEF_COUNT = 27, + PH_MARKER_DEF_COUNT = 18, }; extern const PhAssetDef ph_asset_defs[PH_ASSET_COUNT]; @@ -84,7 +101,6 @@ extern const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT]; extern const PhEntityDef ph_entity_defs[PH_ENTITY_DEF_COUNT]; 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 PhSpellBook ph_player_spellbook; extern const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT]; extern const PhWorldSetup ph_game_world; diff --git a/src/game/main.c b/src/game/main.c @@ -32,7 +32,7 @@ ph_update_game(PhGameContext *context, PhInput input, float dt) fprintf(stderr, "failed to enter area\n"); if (world->encounter_index >= 0) { ph_battle_begin(context->battle, world, world->encounter_index, - ph_player_spellbook); + *context->spells); *game = PH_GAME_TRANSITION; } } @@ -98,8 +98,15 @@ ph_run_game(void) PhShopUi shop_ui = { 0 }; PhGameMode game = PH_GAME_WORLD; PhBattle battle = { 0 }; - PhGameContext context = { &world, &inventory_ui, &shop_ui, &game, &battle, - PH_MENU_NONE }; + PhSpellBook spells = ph_player_spellbook; + PhGameContext context = { + .world = &world, + .inventory = &inventory_ui, + .shop = &shop_ui, + .mode = &game, + .battle = &battle, + .spells = &spells, + }; Uint64 last_ticks; int running = 1; PhMenu menu = PH_MENU_NONE; @@ -192,7 +199,7 @@ ph_run_game(void) menu = PH_MENU_NONE; } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && game == PH_GAME_WORLD && menu == PH_MENU_SHOP) { - if (ph_shop_key(&shop_ui, &world, event.key.scancode)) + if (ph_shop_key(&shop_ui, &world, &spells, event.key.scancode)) menu = PH_MENU_NONE; } } diff --git a/src/game/presentation.c b/src/game/presentation.c @@ -480,7 +480,7 @@ ph_interaction_open(PhShopUi *shop_ui, int interaction_id) } int -ph_shop_key(PhShopUi *ui, PhWorld *world, SDL_Scancode key) +ph_shop_key(PhShopUi *ui, PhWorld *world, PhSpellBook *spells, SDL_Scancode key) { const PhShopDef *shop = ph_game_shop_def(ui->shop_id); int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 || @@ -501,8 +501,9 @@ ph_shop_key(PhShopUi *ui, PhWorld *world, SDL_Scancode key) return 0; } if (previous || next) { - int count = ui->mode == PH_SHOP_ROOT ? 3 : - ui->mode == PH_SHOP_BUY ? shop->item_count : world->content.item_count; + int count = ui->mode == PH_SHOP_ROOT ? + (shop->kind == PH_SHOP_ITEMS ? 3 : 2) : + ui->mode == PH_SHOP_BUY ? shop->stock_count : world->content.item_count; if (ui->mode == PH_SHOP_SELL) { ui->selected = ph_inventory_step(world, ui->selected, @@ -515,18 +516,35 @@ ph_shop_key(PhShopUi *ui, PhWorld *world, SDL_Scancode key) } if (key != PH_KEY_MENU_ACCEPT) return 0; if (ui->mode == PH_SHOP_ROOT) { - ui->mode = (PhShopMode)(ui->selected + 1); + ui->mode = shop->kind == PH_SHOP_ITEMS ? + (PhShopMode)(ui->selected + 1) : + ui->selected == 0 ? PH_SHOP_BUY : PH_SHOP_TALK; ui->selected = ui->mode == PH_SHOP_SELL ? ph_inventory_step(world, -1, 1) : 0; return 0; } - if (ui->mode == PH_SHOP_BUY && ui->selected < shop->item_count) { - const PhItemDef *item = ph_world_item_def(world, shop->items[ui->selected]); - - if (!item || ph_world_buy_item(world, world->player_index, item->id) < 0) - snprintf(ui->status, sizeof(ui->status), "Not enough gold."); - else - snprintf(ui->status, sizeof(ui->status), "Bought %s.", item->name); + if (ui->mode == PH_SHOP_BUY && ui->selected < shop->stock_count) { + if (shop->kind == PH_SHOP_SPELLS) { + const PhSpellDef *spell = ph_spellbook_def(spells, + shop->stock[ui->selected]); + + if (!spell) return 0; + if (ph_spellbook_has(spells, spell->id)) + snprintf(ui->status, sizeof(ui->status), "Already learned."); + else if (ph_spellbook_buy(spells, world, world->player_index, + spell->id) < 0) + snprintf(ui->status, sizeof(ui->status), "Not enough gold."); + else + snprintf(ui->status, sizeof(ui->status), "Learned %s.", spell->name); + } else { + const PhItemDef *item = ph_world_item_def(world, + shop->stock[ui->selected]); + + if (!item || ph_world_buy_item(world, world->player_index, item->id) < 0) + snprintf(ui->status, sizeof(ui->status), "Not enough gold."); + else + snprintf(ui->status, sizeof(ui->status), "Bought %s.", item->name); + } } else if (ui->mode == PH_SHOP_SELL && ui->selected >= 0) { const PhItemDef *item = &world->content.items[ui->selected]; @@ -542,9 +560,11 @@ ph_shop_key(PhShopUi *ui, PhWorld *world, SDL_Scancode key) } static void -ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world, const PhShopUi *ui) +ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world, + const PhSpellBook *spells, const PhShopUi *ui) { - static const char *root_options[] = { "BUY", "SELL", "TALK" }; + static const char *item_options[] = { "BUY", "SELL", "TALK" }; + static const char *magick_options[] = { "LEARN", "TALK" }; const PhShopDef *shop = ph_game_shop_def(ui->shop_id); const PhEntity *player = ph_world_player(world); char text[128]; @@ -555,20 +575,41 @@ ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world, const PhShopUi *ui) snprintf(text, sizeof(text), "GOLD %d", player->gold); SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 38.0f, text); if (ui->mode == PH_SHOP_ROOT) { - for (i = 0; i < (int)LEN(root_options); ++i) { + const char **options = shop->kind == PH_SHOP_ITEMS ? + item_options : magick_options; + int option_count = shop->kind == PH_SHOP_ITEMS ? + (int)LEN(item_options) : (int)LEN(magick_options); + + for (i = 0; i < option_count; ++i) { snprintf(text, sizeof(text), "%c %s", ui->selected == i ? '>' : ' ', - root_options[i]); + options[i]); SDL_RenderDebugText(renderer, 32.0f, 56.0f + (float)i * 20.0f, text); } - SDL_RenderDebugText(renderer, 32.0f, 132.0f, "Welcome, traveler."); + SDL_RenderDebugText(renderer, 32.0f, 132.0f, + shop->kind == PH_SHOP_ITEMS ? "Welcome, traveler." : + "Choose your lesson."); } else if (ui->mode == PH_SHOP_BUY) { - SDL_RenderDebugText(renderer, 24.0f, 42.0f, "BUY"); - for (i = 0; i < shop->item_count; ++i) { - const PhItemDef *item = ph_world_item_def(world, shop->items[i]); - - if (!item) continue; - snprintf(text, sizeof(text), "%c %-18s %d G", - ui->selected == i ? '>' : ' ', item->name, item->buy_value); + SDL_RenderDebugText(renderer, 24.0f, 42.0f, + shop->kind == PH_SHOP_ITEMS ? "BUY" : "MAGICK"); + for (i = 0; i < shop->stock_count; ++i) { + if (shop->kind == PH_SHOP_SPELLS) { + const PhSpellDef *spell = ph_spellbook_def(spells, shop->stock[i]); + + if (!spell) continue; + if (ph_spellbook_has(spells, spell->id)) + snprintf(text, sizeof(text), "%c %-12s LEARNED", + ui->selected == i ? '>' : ' ', spell->name); + else + snprintf(text, sizeof(text), "%c %-12s %4d G %2d MP", + ui->selected == i ? '>' : ' ', spell->name, + spell->buy_value, spell->mp_cost); + } else { + const PhItemDef *item = ph_world_item_def(world, shop->stock[i]); + + if (!item) continue; + snprintf(text, sizeof(text), "%c %-18s %d G", + ui->selected == i ? '>' : ' ', item->name, item->buy_value); + } SDL_RenderDebugText(renderer, 24.0f, 62.0f + (float)i * 20.0f, text); } } else if (ui->mode == PH_SHOP_SELL) { @@ -785,7 +826,7 @@ ph_present_frame(SDL_Renderer *renderer, const PhRenderAssets *assets, } else if (context->menu == PH_MENU_INVENTORY) { ph_draw_inventory(renderer, assets, world, context->inventory); } else if (context->menu == PH_MENU_SHOP) { - ph_draw_shop(renderer, world, context->shop); + ph_draw_shop(renderer, world, context->spells, context->shop); } else if (world->notice.seconds > 0.0f && world->notice.text[0] != '\0') { SDL_FRect notice_bg = { .x = 8.0f, diff --git a/src/game/presentation.h b/src/game/presentation.h @@ -64,6 +64,7 @@ typedef struct { PhShopUi *shop; PhGameMode *mode; PhBattle *battle; + PhSpellBook *spells; PhMenu menu; } PhGameContext; @@ -72,7 +73,8 @@ void ph_inventory_open(PhInventoryUi *ui, const PhWorld *world); int ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key); void ph_shop_open(PhShopUi *ui, int shop_id); PhMenu ph_interaction_open(PhShopUi *shop_ui, int interaction_id); -int ph_shop_key(PhShopUi *ui, PhWorld *world, SDL_Scancode key); +int ph_shop_key(PhShopUi *ui, PhWorld *world, PhSpellBook *spells, + SDL_Scancode key); void ph_present_frame(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhGameContext *context); diff --git a/src/tests/render.c b/src/tests/render.c @@ -19,8 +19,15 @@ main(void) PhShopUi shop_ui = { 0 }; PhGameMode game = PH_GAME_WORLD; PhBattle battle = { 0 }; - PhGameContext context = { &world, &inventory_ui, &shop_ui, &game, &battle, - PH_MENU_NONE }; + PhSpellBook spells = ph_player_spellbook; + PhGameContext context = { + .world = &world, + .inventory = &inventory_ui, + .shop = &shop_ui, + .mode = &game, + .battle = &battle, + .spells = &spells, + }; int i; int enemy_index = -1; int result = 0; @@ -131,6 +138,21 @@ main(void) context.menu = PH_MENU_SHOP; ph_present_frame(renderer, &assets, &context); } + ph_shop_open(&shop_ui, PH_SHOP_MAGICK); + shop_ui.mode = PH_SHOP_ROOT; + ph_present_frame(renderer, &assets, &context); + ph_shop_key(&shop_ui, &world, &spells, PH_KEY_MENU_ACCEPT); + if (shop_ui.mode != PH_SHOP_BUY) result = 1; + ph_present_frame(renderer, &assets, &context); + ph_shop_key(&shop_ui, &world, &spells, PH_KEY_MENU_ACCEPT); + if (!ph_spellbook_has(&spells, PH_SPELL_EMBER) || + ph_world_player(&world)->gold != 16) { + fprintf(stderr, "render smoke test failed: magick purchase\n"); + result = 1; + } + ph_present_frame(renderer, &assets, &context); + shop_ui.mode = PH_SHOP_TALK; + ph_present_frame(renderer, &assets, &context); for (i = 0; i < world.entity_count; ++i) { const PhEntityDef *def = ph_world_entity_def(&world, world.entities[i].type_id); if (def && def->kind == PH_ENTITY_MONSTER) enemy_index = i; @@ -142,7 +164,7 @@ main(void) world.encounter_index = enemy_index; game = PH_GAME_WORLD; context.menu = PH_MENU_NONE; - ph_battle_begin(&battle, &world, enemy_index, ph_player_spellbook); + ph_battle_begin(&battle, &world, enemy_index, spells); game = PH_GAME_TRANSITION; ph_present_frame(renderer, &assets, &context); if (game != PH_GAME_TRANSITION) result = 1; @@ -161,7 +183,7 @@ main(void) if (game != PH_GAME_OVER) result = 1; world.entities[world.player_index].vitals.hp = ph_world_entity_stats(&world, ph_world_player(&world)).max_hp; - ph_battle_begin(&battle, &world, enemy_index, ph_player_spellbook); + ph_battle_begin(&battle, &world, enemy_index, spells); game = PH_GAME_TRANSITION; context.menu = PH_MENU_NONE; if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) == diff --git a/src/tests/smoke.c b/src/tests/smoke.c @@ -327,6 +327,7 @@ ph_area_shop_test(void) PhEntity *player; const PhItemDef *potion; const PhItemDef *ether; + PhSpellBook spells = ph_player_spellbook; int potion_before; int slime_found = 0; int shop_id = -1; @@ -343,10 +344,11 @@ ph_area_shop_test(void) 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, 8, 2) || + ph_area_tile_blocked(&world.area, 8, 2) || + !ph_area_tile_blocked(&world.area, 13, 2) || ph_area_tile_blocked(&world.area, 4, 4) || ph_area_tile_blocked(&world.area, 6, 6)) goto fail; - player->pos = (PhVec2){ PH_TILE_CENTER(8), PH_TILE_CENTER(3) }; + player->pos = (PhVec2){ PH_TILE_CENTER(13), PH_TILE_CENTER(3) }; player->motion.facing_x = 0; player->motion.facing_y = -1; ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); @@ -355,8 +357,11 @@ ph_area_shop_test(void) world.area_id != PH_AREA_MIRA_SHOP || world.initialized_area_count != 3) goto fail; if (ph_area_tile_blocked(&world.area, 0, 0) || - ph_area_tile_blocked(&world.area, world.area.width - 1, + ph_area_tile_blocked(&world.area, 0, 1) || + !ph_area_tile_blocked(&world.area, 0, world.area.height - 1) || + !ph_area_tile_blocked(&world.area, world.area.width - 1, world.area.height - 1) || + ph_area_tile_blocked(&world.area, 10, world.area.height - 1) || !ph_area_tile_blocked(&world.area, -1, 0) || !ph_area_tile_blocked(&world.area, world.area.width, 0)) goto fail; for (i = 0; i < world.area.marker_count; ++i) { @@ -406,8 +411,46 @@ ph_area_shop_test(void) 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_MAGICK_SHOP) != 1 || + world.area_id != PH_AREA_MAGICK_SHOP || + world.initialized_area_count != 4) 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 (!interaction || interaction->kind != PH_INTERACTION_SHOP || + interaction->content_id != PH_SHOP_MAGICK || + world.entities[i].area_id != PH_AREA_MAGICK_SHOP) continue; + player->pos = (PhVec2){ world.entities[i].pos.x, + world.entities[i].pos.y + PH_TILE_SIZE }; + player->motion.facing_x = 0; + player->motion.facing_y = -1; + break; + } + if (i == world.entity_count) goto fail; + ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); + { + const PhInteractionDef *interaction = + ph_game_interaction_def(world.interaction_id); + + if (!interaction || interaction->content_id != PH_SHOP_MAGICK) goto fail; + } + if (!ph_spellbook_has(&spells, PH_SPELL_FLASH) || + ph_spellbook_has(&spells, PH_SPELL_EMBER) || + ph_spellbook_buy(&spells, &world, world.player_index, + PH_SPELL_EMBER) < 0 || + !ph_spellbook_has(&spells, PH_SPELL_EMBER) || player->gold != 0 || + ph_spellbook_buy(&spells, &world, world.player_index, + PH_SPELL_EMBER) == 0 || + ph_spellbook_buy(&spells, &world, world.player_index, + PH_SPELL_METEOR) == 0) goto fail; + if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 || + world.area_id != PH_AREA_STONEHOLLOW || + world.initialized_area_count != 4 || ph_enter_portal(&world, PH_AREA_MEADOW) != 1 || - world.area_id != PH_AREA_MEADOW || world.initialized_area_count != 3) + world.area_id != PH_AREA_MEADOW || world.initialized_area_count != 4) goto fail; for (i = 0; i < world.entity_count; ++i) { const PhEntityDef *def = ph_world_entity_def(&world,