commit 5485df2448c3d97cdbbbc6fa49169d20cced16fb
parent ae151fed5672c21a4e3f470304b661c8b6f30012
Author: beep <beep@wimdupont.com>
Date: Thu, 30 Jul 2026 10:09:22 +0000
Add reusable magick shop system
Diffstat:
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,