phantasia

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

commit df6f8a604f475793cfaa0b12eef7f53d01c9a8cd
parent d1abfae121fdca02aba9e0d6d9133c21a7da1e94
Author: beep <beep@wimdupont.com>
Date:   Wed, 29 Jul 2026 12:11:09 +0000

Add navigable inventory actions

Diffstat:
Mconfig.def.h | 26++++++++++++++++++++++++++
Mphantasia.6 | 4++++
Msrc/engine/world.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/engine/world.h | 11+++++++++++
Msrc/game/main.c | 378+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
5 files changed, 453 insertions(+), 41 deletions(-)

diff --git a/config.def.h b/config.def.h @@ -25,6 +25,7 @@ #define PH_MENU_BORDER_COLOR 116, 126, 148, 255 #define PH_MENU_TEXT_COLOR 232, 236, 244, 255 #define PH_MENU_BONUS_COLOR 126, 220, 146, 255 +#define PH_MENU_DISABLED_COLOR 104, 110, 124, 255 enum { PH_ASSET_NONE, @@ -58,6 +59,7 @@ enum { PH_ITEM_RUSTED_SPEAR = 1, PH_ITEM_WAYFARER_BLADE, PH_ITEM_TRAVELER_COAT, + PH_ITEM_HEALING_POTION, }; static const PhEntityDef ph_entity_defs[] = { @@ -85,6 +87,7 @@ static const PhItemDef ph_item_defs[] = { { .id = PH_ITEM_RUSTED_SPEAR, .name = "Rusted Spear", + .description = "A worn but sturdy spear.", .value = 12, .bonuses = { .attack = 2 }, .equip_slot = PH_EQUIP_WEAPON, @@ -95,6 +98,7 @@ static const PhItemDef ph_item_defs[] = { { .id = PH_ITEM_WAYFARER_BLADE, .name = "Wayfarer Blade", + .description = "A reliable traveler's blade.", .value = 20, .bonuses = { .attack = 3 }, .equip_slot = PH_EQUIP_WEAPON, @@ -105,6 +109,7 @@ static const PhItemDef ph_item_defs[] = { { .id = PH_ITEM_TRAVELER_COAT, .name = "Traveler Coat", + .description = "A coat hardened by the road.", .value = 18, .bonuses = { .max_hp = 5, .defense = 2 }, .equip_slot = PH_EQUIP_ARMOR, @@ -112,6 +117,17 @@ static const PhItemDef ph_item_defs[] = { .sprite_tile_x = 4, .sprite_tile_y = 6, }, + { + .id = PH_ITEM_HEALING_POTION, + .name = "Healing Potion", + .description = "Restores 15 HP.", + .value = 8, + .use = PH_ITEM_USE_HEAL, + .use_power = 15, + .asset_id = PH_ASSET_ITEMS, + .sprite_tile_x = 3, + .sprite_tile_y = 0, + }, }; static const int ph_player_equipment[PH_EQUIP_COUNT] = { @@ -135,6 +151,7 @@ static const struct { int amount; } ph_item_drops[] = { { PH_ITEM_RUSTED_SPEAR, { 104.0f, 80.0f }, 1 }, + { PH_ITEM_HEALING_POTION, { 104.0f, 96.0f }, 1 }, }; #if PH_USE_SDL @@ -150,6 +167,15 @@ static const struct { #define PH_KEY_INTERACT_2 SDL_SCANCODE_SPACE #define PH_KEY_CHARACTER SDL_SCANCODE_C #define PH_KEY_INVENTORY SDL_SCANCODE_I +#define PH_KEY_MENU_UP_1 SDL_SCANCODE_UP +#define PH_KEY_MENU_UP_2 SDL_SCANCODE_K +#define PH_KEY_MENU_DOWN_1 SDL_SCANCODE_DOWN +#define PH_KEY_MENU_DOWN_2 SDL_SCANCODE_J +#define PH_KEY_MENU_LEFT_1 SDL_SCANCODE_LEFT +#define PH_KEY_MENU_LEFT_2 SDL_SCANCODE_H +#define PH_KEY_MENU_RIGHT_1 SDL_SCANCODE_RIGHT +#define PH_KEY_MENU_RIGHT_2 SDL_SCANCODE_L +#define PH_KEY_MENU_ACCEPT SDL_SCANCODE_RETURN #endif #endif diff --git a/phantasia.6 b/phantasia.6 @@ -22,6 +22,10 @@ Interact and pick up nearby loot. Open or close the character sheet. .It I Open or close the inventory. +.It Arrow keys / HJKL +Move through inventory items and actions. +.It Enter +Open the selected item's action menu or confirm its selected action. .It Window close button Quit the game. .El diff --git a/src/engine/world.c b/src/engine/world.c @@ -325,22 +325,28 @@ ph_world_spawn_entity(PhWorld *world, int type_id, PhVec2 pos) int ph_world_drop_item(PhWorld *world, int item_id, PhVec2 pos, int amount) { - PhGroundItem *drop; + PhGroundItem *drop = NULL; + int i; - if (world->ground_item_count >= PH_MAX_GROUND_ITEMS) { - return -1; - } if (amount <= 0 || !ph_world_item_def(world, item_id)) { return -1; } + for (i = 0; i < world->ground_item_count; ++i) + if (!world->ground_items[i].active) { + drop = &world->ground_items[i]; + break; + } + if (!drop) { + if (world->ground_item_count >= PH_MAX_GROUND_ITEMS) return -1; + drop = &world->ground_items[world->ground_item_count++]; + } - drop = &world->ground_items[world->ground_item_count]; drop->item_id = item_id; drop->pos = pos; drop->amount = amount; drop->active = 1; - return world->ground_item_count++; + return (int)(drop - world->ground_items); } void @@ -376,6 +382,63 @@ ph_world_equip_item(PhWorld *world, int entity_index, int item_id) return 0; } +int +ph_world_equip_inventory_item(PhWorld *world, int entity_index, int item_id) +{ + const PhItemDef *item = ph_world_item_def(world, item_id); + PhEntity *entity; + int item_index; + int old_index; + int old_item; + + if (!item || entity_index < 0 || entity_index >= world->entity_count) return -1; + item_index = ph_item_def_index(world, item_id); + if (world->inventory[item_index] <= 0 || item->equip_slot <= PH_EQUIP_NONE || + item->equip_slot >= PH_EQUIP_COUNT) return -1; + entity = &world->entities[entity_index]; + old_item = entity->equipment[item->equip_slot]; + if (ph_world_equip_item(world, entity_index, item_id) < 0) return -1; + --world->inventory[item_index]; + old_index = ph_item_def_index(world, old_item); + if (old_index >= 0) ++world->inventory[old_index]; + return 0; +} + +int +ph_world_use_inventory_item(PhWorld *world, int entity_index, int item_id) +{ + const PhItemDef *item = ph_world_item_def(world, item_id); + PhEntity *entity; + PhStats stats; + int item_index; + + if (!item || entity_index < 0 || entity_index >= world->entity_count) return -1; + item_index = ph_item_def_index(world, item_id); + if (world->inventory[item_index] <= 0) return -1; + entity = &world->entities[entity_index]; + stats = ph_world_entity_stats(world, entity); + if (item->use != PH_ITEM_USE_HEAL || item->use_power <= 0 || + entity->hp >= stats.max_hp) return -1; + entity->hp += item->use_power; + if (entity->hp > stats.max_hp) entity->hp = stats.max_hp; + --world->inventory[item_index]; + return 0; +} + +int +ph_world_drop_inventory_item(PhWorld *world, int entity_index, int item_id) +{ + int item_index; + + if (entity_index < 0 || entity_index >= world->entity_count) return -1; + item_index = ph_item_def_index(world, item_id); + if (item_index < 0 || world->inventory[item_index] <= 0) return -1; + if (ph_world_drop_item(world, item_id, world->entities[entity_index].pos, 1) < 0) + return -1; + --world->inventory[item_index]; + return 0; +} + void ph_world_tick(PhWorld *world, PhInput input, float dt) { diff --git a/src/engine/world.h b/src/engine/world.h @@ -24,6 +24,11 @@ typedef enum { } PhEquipSlot; typedef enum { + PH_ITEM_USE_NONE, + PH_ITEM_USE_HEAL, +} PhItemUse; + +typedef enum { PH_ENTITY_PLAYER = 1, PH_ENTITY_MONSTER, PH_ENTITY_NPC, @@ -69,9 +74,12 @@ typedef struct { typedef struct { int id; const char *name; + const char *description; int value; PhStats bonuses; PhEquipSlot equip_slot; + PhItemUse use; + int use_power; int asset_id; int sprite_tile_x; int sprite_tile_y; @@ -141,6 +149,9 @@ int ph_world_spawn_entity(PhWorld *world, int type_id, PhVec2 pos); int ph_world_drop_item(PhWorld *world, int item_id, PhVec2 pos, int amount); void ph_world_set_player(PhWorld *world, int entity_index); int ph_world_equip_item(PhWorld *world, int entity_index, int item_id); +int ph_world_equip_inventory_item(PhWorld *world, int entity_index, int item_id); +int ph_world_use_inventory_item(PhWorld *world, int entity_index, int item_id); +int ph_world_drop_inventory_item(PhWorld *world, int entity_index, int item_id); void ph_world_tick(PhWorld *world, PhInput input, float dt); const PhEntity *ph_world_player(const PhWorld *world); diff --git a/src/game/main.c b/src/game/main.c @@ -10,6 +10,33 @@ #include "config.h" +#if PH_USE_SDL +#ifndef PH_MENU_COLOR +#define PH_MENU_COLOR PH_SHEET_COLOR +#define PH_MENU_BORDER_COLOR PH_SHEET_BORDER_COLOR +#define PH_MENU_TEXT_COLOR PH_SHEET_TEXT_COLOR +#define PH_MENU_BONUS_COLOR PH_SHEET_BONUS_COLOR +#define PH_MENU_DISABLED_COLOR PH_SHEET_BORDER_COLOR +#endif +#ifndef PH_MENU_DISABLED_COLOR +#define PH_MENU_DISABLED_COLOR PH_MENU_BORDER_COLOR +#endif +#ifndef PH_KEY_INVENTORY +#define PH_KEY_INVENTORY SDL_SCANCODE_I +#endif +#ifndef PH_KEY_MENU_UP_1 +#define PH_KEY_MENU_UP_1 SDL_SCANCODE_UP +#define PH_KEY_MENU_UP_2 SDL_SCANCODE_K +#define PH_KEY_MENU_DOWN_1 SDL_SCANCODE_DOWN +#define PH_KEY_MENU_DOWN_2 SDL_SCANCODE_J +#define PH_KEY_MENU_LEFT_1 SDL_SCANCODE_LEFT +#define PH_KEY_MENU_LEFT_2 SDL_SCANCODE_H +#define PH_KEY_MENU_RIGHT_1 SDL_SCANCODE_RIGHT +#define PH_KEY_MENU_RIGHT_2 SDL_SCANCODE_L +#define PH_KEY_MENU_ACCEPT SDL_SCANCODE_RETURN +#endif +#endif + #define LEN(a) (sizeof(a) / sizeof(0[a])) #if PH_USE_SDL @@ -22,6 +49,27 @@ typedef enum { PH_MENU_CHARACTER, PH_MENU_INVENTORY, } PhMenu; + +typedef enum { + PH_INVENTORY_BROWSE, + PH_INVENTORY_ACTIONS, + PH_INVENTORY_EXAMINE, +} PhInventoryMode; + +typedef enum { + PH_INVENTORY_EQUIP, + PH_INVENTORY_USE, + PH_INVENTORY_DROP, + PH_INVENTORY_EXAMINE_ACTION, + PH_INVENTORY_ACTION_COUNT, +} PhInventoryAction; + +typedef struct { + int selected; + int option; + PhInventoryMode mode; + char status[64]; +} PhInventoryUi; #endif #if PH_USE_SDL @@ -95,23 +143,38 @@ ph_run_smoke_test(void) tiles[5] = tiles[6] = (unsigned char)open; ph_world_init(&world, area, 64.0f, 48.0f); if (ph_world_add_entity_def(&world, (PhEntityDef){ - .id = 1, .name = "Tester", .stats = { .max_hp = 1, .speed = 1 }, + .id = 1, .name = "Tester", .stats = { .max_hp = 5, .speed = 1 }, .kind = PH_ENTITY_PLAYER }) < 0 || ph_world_add_item_def(&world, (PhItemDef){ .id = 1, .bonuses = { .max_hp = 2, .attack = 2 }, .equip_slot = PH_EQUIP_WEAPON }) < 0 || + ph_world_add_item_def(&world, (PhItemDef){ + .id = 2, .use = PH_ITEM_USE_HEAL, .use_power = 3 }) < 0 || + ph_world_add_item_def(&world, (PhItemDef){ + .id = 3, .equip_slot = PH_EQUIP_WEAPON }) < 0 || (player_index = ph_world_spawn_entity(&world, 1, (PhVec2){ 24.0f, 24.0f })) < 0 || - ph_world_drop_item(&world, 1, (PhVec2){ 24.0f, 24.0f }, 1) < 0) { + ph_world_drop_item(&world, 1, (PhVec2){ 24.0f, 24.0f }, 1) < 0 || + ph_world_drop_item(&world, 2, (PhVec2){ 24.0f, 24.0f }, 2) < 0) { fprintf(stderr, "smoke test failed: setup\n"); return 1; } ph_world_set_player(&world, player_index); - if (ph_world_equip_item(&world, player_index, 1) < 0) { - fprintf(stderr, "smoke test failed: equipment\n"); + if (ph_world_equip_item(&world, player_index, 3) < 0) { + fprintf(stderr, "smoke test failed: starting equipment\n"); return 1; } ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); + if (ph_world_equip_inventory_item(&world, player_index, 1) < 0) { + fprintf(stderr, "smoke test failed: inventory equipment\n"); + return 1; + } + world.entities[player_index].hp = 1; + if (ph_world_use_inventory_item(&world, player_index, 2) < 0 || + ph_world_drop_inventory_item(&world, player_index, 2) < 0) { + fprintf(stderr, "smoke test failed: inventory item action\n"); + return 1; + } for (i = 0; i < 30; ++i) { ph_world_tick(&world, (PhInput){ .move_x = 1 }, 1.0f); @@ -122,15 +185,19 @@ ph_run_smoke_test(void) stats = ph_world_entity_stats(&world, player); if (!player || player->pos.x != 47.0f || player->pos.y != 25.0f || world.camera.pos.x != 0.0f || world.camera.pos.y != 0.0f || - ph_world_item_amount(&world, 1) != 1 || player->hp != 3 || - stats.max_hp != 3 || stats.attack != 2 || - player->equipment[PH_EQUIP_WEAPON] != 1) { + ph_world_item_amount(&world, 1) != 0 || + ph_world_item_amount(&world, 2) != 0 || + ph_world_item_amount(&world, 3) != 1 || player->hp != 4 || + stats.max_hp != 7 || stats.attack != 2 || + player->equipment[PH_EQUIP_WEAPON] != 1 || + !world.ground_items[0].active || world.ground_items[0].item_id != 2) { fprintf(stderr, "smoke test failed: unexpected world state\n"); return 1; } - printf("smoke player=(%.1f,%.1f) item=%d\n", - player->pos.x, player->pos.y, ph_world_item_amount(&world, 1)); + printf("smoke player=(%.1f,%.1f) hp=%d equipment=%d\n", + player->pos.x, player->pos.y, player->hp, + player->equipment[PH_EQUIP_WEAPON]); return 0; } @@ -430,51 +497,262 @@ ph_draw_character_sheet(SDL_Renderer *renderer, const PhAssets *assets, } } +static int +ph_inventory_step(const PhWorld *world, int selected, int direction) +{ + int i = selected < 0 ? (direction > 0 ? -1 : 0) : selected; + int step; + + for (step = 0; step < world->item_def_count; ++step) { + i += direction; + if (i < 0) i = world->item_def_count - 1; + if (i >= world->item_def_count) i = 0; + if (world->inventory[i] > 0) return i; + } + return -1; +} + +static void +ph_inventory_open(PhInventoryUi *ui, const PhWorld *world) +{ + memset(ui, 0, sizeof(*ui)); + ui->selected = ph_inventory_step(world, -1, 1); +} + +static void +ph_inventory_reselect(PhInventoryUi *ui, const PhWorld *world) +{ + if (ui->selected >= 0 && world->inventory[ui->selected] > 0) return; + ui->selected = ph_inventory_step(world, ui->selected, 1); +} + static void -ph_draw_inventory(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *world) +ph_inventory_action(PhInventoryUi *ui, PhWorld *world, int action) { + const PhItemDef *item; + const char *name; + int result = -1; + + if (ui->selected < 0) return; + item = &world->item_defs[ui->selected]; + name = item->name ? item->name : "Item"; + if (action == PH_INVENTORY_EQUIP) + result = ph_world_equip_inventory_item(world, world->player_index, item->id); + else if (action == PH_INVENTORY_USE) + result = ph_world_use_inventory_item(world, world->player_index, item->id); + else if (action == PH_INVENTORY_DROP) + result = ph_world_drop_inventory_item(world, world->player_index, item->id); + else { + ui->mode = PH_INVENTORY_EXAMINE; + ui->status[0] = '\0'; + return; + } + + if (result < 0) { + snprintf(ui->status, sizeof(ui->status), "Cannot %s %s.", + action == PH_INVENTORY_EQUIP ? "equip" : + action == PH_INVENTORY_USE ? "use" : "drop", name); + return; + } + snprintf(ui->status, sizeof(ui->status), "%s %s.", + action == PH_INVENTORY_EQUIP ? "Equipped" : + action == PH_INVENTORY_USE ? "Used" : "Dropped", name); + ui->mode = PH_INVENTORY_BROWSE; + ph_inventory_reselect(ui, world); +} + +static int +ph_inventory_action_enabled(const PhItemDef *item, int action) +{ + if (action == PH_INVENTORY_EQUIP) return item->equip_slot != PH_EQUIP_NONE; + if (action == PH_INVENTORY_USE) return item->use != PH_ITEM_USE_NONE; + return 1; +} + +static void +ph_inventory_action_order(const PhItemDef *item, int actions[PH_INVENTORY_ACTION_COUNT]) +{ + if (item->equip_slot != PH_EQUIP_NONE) { + actions[0] = PH_INVENTORY_EQUIP; + actions[1] = PH_INVENTORY_USE; + } else if (item->use != PH_ITEM_USE_NONE) { + actions[0] = PH_INVENTORY_USE; + actions[1] = PH_INVENTORY_EQUIP; + } else { + actions[0] = PH_INVENTORY_DROP; + actions[1] = PH_INVENTORY_EXAMINE_ACTION; + } + if (actions[0] < PH_INVENTORY_DROP) { + actions[2] = PH_INVENTORY_DROP; + actions[3] = PH_INVENTORY_EXAMINE_ACTION; + } else { + actions[2] = PH_INVENTORY_EQUIP; + actions[3] = PH_INVENTORY_USE; + } +} + +static int +ph_inventory_action_step(const PhItemDef *item, int selected, int direction) +{ + int actions[PH_INVENTORY_ACTION_COUNT]; + int position = 0; + int step; + + ph_inventory_action_order(item, actions); + while (position < PH_INVENTORY_ACTION_COUNT && actions[position] != selected) ++position; + for (step = 0; step < PH_INVENTORY_ACTION_COUNT; ++step) { + position = (position + (direction < 0 ? + PH_INVENTORY_ACTION_COUNT - 1 : 1)) % PH_INVENTORY_ACTION_COUNT; + if (ph_inventory_action_enabled(item, actions[position])) return actions[position]; + } + return selected; +} + +static void +ph_draw_inventory_popup(SDL_Renderer *renderer, const PhWorld *world, + const PhInventoryUi *ui) +{ + static const char *action_names[] = { "EQUIP", "USE", "DROP", "EXAMINE" }; + const PhItemDef *item = &world->item_defs[ui->selected]; + int actions[PH_INVENTORY_ACTION_COUNT]; + SDL_FRect popup = ui->mode == PH_INVENTORY_EXAMINE ? + (SDL_FRect){ 40.0f, 48.0f, 256.0f, 124.0f } : + (SDL_FRect){ 156.0f, 48.0f, 140.0f, 124.0f }; char text[128]; - int shown = 0; - int more = 0; + float x = ui->mode == PH_INVENTORY_EXAMINE ? 52.0f : 168.0f; int i; + SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR); + SDL_RenderFillRect(renderer, &popup); + SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR); + SDL_RenderRect(renderer, &popup); + SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); + if (ui->mode == PH_INVENTORY_EXAMINE) { + SDL_RenderDebugText(renderer, x, 58.0f, + item->name ? item->name : "ITEM"); + SDL_RenderDebugText(renderer, x, 76.0f, + item->description ? item->description : "No description."); + snprintf(text, sizeof(text), "VALUE %d", item->value); + SDL_RenderDebugText(renderer, x, 96.0f, text); + ph_format_bonuses(text, sizeof(text), item->bonuses); + if (item->use == PH_ITEM_USE_HEAL) + snprintf(text, sizeof(text), "HEALS %d HP", item->use_power); + if (text[0]) SDL_RenderDebugText(renderer, x, 112.0f, text); + SDL_RenderDebugText(renderer, x, 148.0f, "ENTER BACK"); + return; + } + + SDL_RenderDebugText(renderer, 168.0f, 58.0f, "ITEM ACTION"); + ph_inventory_action_order(item, actions); + for (i = 0; i < PH_INVENTORY_ACTION_COUNT; ++i) { + int action = actions[i]; + + if (ph_inventory_action_enabled(item, action)) + SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); + else + SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR); + snprintf(text, sizeof(text), "%c %s", + action == ui->option ? '>' : ' ', action_names[action]); + SDL_RenderDebugText(renderer, 168.0f, 78.0f + (float)i * 18.0f, text); + } +} + +static void +ph_draw_inventory(SDL_Renderer *renderer, const PhAssets *assets, + const PhWorld *world, const PhInventoryUi *ui) +{ + char text[128]; + int selected_rank = 0; + int total = 0; + int rank = 0; + int page; + int i; + + for (i = 0; i < world->item_def_count; ++i) + if (world->inventory[i] > 0) { + if (i == ui->selected) selected_rank = total; + ++total; + } + page = selected_rank / 5; ph_draw_menu(renderer, "INVENTORY", PH_KEY_INVENTORY); for (i = 0; i < world->item_def_count; ++i) { const PhItemDef *item = &world->item_defs[i]; - int amount = world->inventory[i]; float y; - if (amount <= 0) continue; - if (shown >= 6) { - ++more; + if (world->inventory[i] <= 0) continue; + if (rank < page * 5 || rank >= page * 5 + 5) { + ++rank; continue; } - y = 48.0f + (float)shown++ * 27.0f; + y = 48.0f + (float)(rank++ - page * 5) * 27.0f; + SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); + SDL_RenderDebugText(renderer, 20.0f, y, i == ui->selected ? ">" : " "); if (item->asset_id > PH_ASSET_NONE && (size_t)item->asset_id < LEN(assets->textures)) { - SDL_FRect icon = { 24.0f, y - 4.0f, 16.0f, 16.0f }; + SDL_FRect icon = { 32.0f, y - 4.0f, 16.0f, 16.0f }; ph_draw_sprite(renderer, assets->textures[item->asset_id], item->sprite_tile_x, item->sprite_tile_y, icon, SDL_FLIP_NONE); } - SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); - snprintf(text, sizeof(text), "%s x%d", item->name ? item->name : "Unknown", amount); - SDL_RenderDebugText(renderer, 48.0f, y, text); + snprintf(text, sizeof(text), "%s x%d", + item->name ? item->name : "Unknown", world->inventory[i]); + SDL_RenderDebugText(renderer, 56.0f, y, text); ph_format_bonuses(text, sizeof(text), item->bonuses); + if (!text[0] && item->use == PH_ITEM_USE_HEAL) + snprintf(text, sizeof(text), "HEALS %d HP", item->use_power); if (!text[0]) snprintf(text, sizeof(text), "VALUE %d", item->value); SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR); - SDL_RenderDebugText(renderer, 48.0f, y + 10.0f, text); + SDL_RenderDebugText(renderer, 56.0f, y + 10.0f, text); } SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); - if (!shown) SDL_RenderDebugText(renderer, 24.0f, 48.0f, "EMPTY"); - if (more) { - snprintf(text, sizeof(text), "+%d MORE", more); - SDL_RenderDebugText(renderer, 24.0f, 208.0f, text); + if (!total) SDL_RenderDebugText(renderer, 24.0f, 48.0f, "EMPTY"); + if (total > 5) { + snprintf(text, sizeof(text), "PAGE %d/%d", page + 1, (total + 4) / 5); + SDL_RenderDebugText(renderer, 24.0f, 184.0f, text); + } + if (ui->status[0]) SDL_RenderDebugText(renderer, 24.0f, 200.0f, ui->status); + snprintf(text, sizeof(text), "%s SELECT", + SDL_GetScancodeName(PH_KEY_MENU_ACCEPT)); + SDL_RenderDebugText(renderer, 24.0f, 216.0f, text); + if (ui->selected >= 0 && ui->mode != PH_INVENTORY_BROWSE) + ph_draw_inventory_popup(renderer, world, ui); +} + +static void +ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key) +{ + int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 || + key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2; + int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 || + key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2; + + if (key == PH_KEY_MENU_ACCEPT) { + if (ui->mode == PH_INVENTORY_BROWSE && ui->selected >= 0) { + int actions[PH_INVENTORY_ACTION_COUNT]; + + ph_inventory_action_order(&world->item_defs[ui->selected], actions); + ui->mode = PH_INVENTORY_ACTIONS; + ui->option = actions[0]; + ui->status[0] = '\0'; + } else if (ui->mode == PH_INVENTORY_ACTIONS) { + ph_inventory_action(ui, world, ui->option); + } else if (ui->mode == PH_INVENTORY_EXAMINE) { + ui->mode = PH_INVENTORY_ACTIONS; + } + return; + } + if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return; + if (ui->mode == PH_INVENTORY_ACTIONS) { + ui->option = ph_inventory_action_step(&world->item_defs[ui->selected], + ui->option, previous ? -1 : 1); + } else { + ui->selected = ph_inventory_step(world, ui->selected, previous ? -1 : 1); + ui->status[0] = '\0'; } } static void ph_render_frame(SDL_Renderer *renderer, const PhAssets *assets, PhWorld *world, - PhInput input, float dt, PhMenu menu) + PhInput input, float dt, PhMenu menu, const PhInventoryUi *inventory_ui) { if (menu == PH_MENU_NONE) ph_world_tick(world, input, dt); @@ -486,7 +764,7 @@ ph_render_frame(SDL_Renderer *renderer, const PhAssets *assets, PhWorld *world, if (menu == PH_MENU_CHARACTER) { ph_draw_character_sheet(renderer, assets, world); } else if (menu == PH_MENU_INVENTORY) { - ph_draw_inventory(renderer, assets, world); + ph_draw_inventory(renderer, assets, world, inventory_ui); } else if (world->notice_seconds > 0.0f && world->notice[0] != '\0') { SDL_FRect notice_bg = { .x = 8.0f, @@ -561,6 +839,7 @@ ph_run_render_smoke_test(void) SDL_Renderer *renderer; PhAssets assets; PhWorld world; + PhInventoryUi inventory_ui; int i; int result = 0; @@ -601,14 +880,34 @@ ph_run_render_smoke_test(void) ph_world_tick(&world, (PhInput){ .move_x = 1 }, 0.1f); ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); - if (ph_world_item_amount(&world, PH_ITEM_RUSTED_SPEAR) != 1) { - fprintf(stderr, "render smoke test failed: spear pickup\n"); + ph_world_tick(&world, (PhInput){ .move_y = 1 }, 0.18f); + 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) { + fprintf(stderr, "render smoke test failed: item pickup\n"); result = 1; } - for (i = 0; i < 8; ++i) { + ph_inventory_open(&inventory_ui, &world); + ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_ACCEPT); + if (inventory_ui.option != PH_INVENTORY_EQUIP) result = 1; + ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1); + if (inventory_ui.option != PH_INVENTORY_DROP) result = 1; + inventory_ui.mode = PH_INVENTORY_BROWSE; + ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1); + ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_ACCEPT); + if (inventory_ui.option != PH_INVENTORY_USE) result = 1; + ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1); + if (inventory_ui.option != PH_INVENTORY_DROP) { + fprintf(stderr, "render smoke test failed: inventory actions\n"); + result = 1; + } + ph_inventory_open(&inventory_ui, &world); + for (i = 0; i < 10; ++i) { + inventory_ui.mode = i == 8 ? PH_INVENTORY_EXAMINE : + i == 9 ? PH_INVENTORY_ACTIONS : PH_INVENTORY_BROWSE; ph_render_frame(renderer, &assets, &world, (PhInput){ 0 }, - 1.0f / 60.0f, i == 6 ? PH_MENU_CHARACTER : - i == 7 ? PH_MENU_INVENTORY : PH_MENU_NONE); + 1.0f / 60.0f, i == 7 ? PH_MENU_CHARACTER : + i >= 6 ? PH_MENU_INVENTORY : PH_MENU_NONE, &inventory_ui); } if (!SDL_SaveBMP(surface, PH_RENDER_SMOKE_PATH)) { @@ -631,6 +930,7 @@ ph_run_game(int frame_limit) SDL_Renderer *renderer; PhAssets assets; PhWorld world; + PhInventoryUi inventory_ui = { 0 }; Uint64 last_ticks; int frame_count = 0; int running = 1; @@ -680,7 +980,15 @@ ph_run_game(int frame_limit) menu = menu == PH_MENU_CHARACTER ? PH_MENU_NONE : PH_MENU_CHARACTER; } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == PH_KEY_INVENTORY) { - menu = menu == PH_MENU_INVENTORY ? PH_MENU_NONE : PH_MENU_INVENTORY; + if (menu == PH_MENU_INVENTORY) { + menu = PH_MENU_NONE; + } else { + menu = PH_MENU_INVENTORY; + ph_inventory_open(&inventory_ui, &world); + } + } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && + menu == PH_MENU_INVENTORY) { + ph_inventory_key(&inventory_ui, &world, event.key.scancode); } } @@ -692,7 +1000,7 @@ ph_run_game(int frame_limit) last_ticks = now_ticks; input = ph_read_input(SDL_GetKeyboardState(NULL)); - ph_render_frame(renderer, &assets, &world, input, dt, menu); + ph_render_frame(renderer, &assets, &world, input, dt, menu, &inventory_ui); ++frame_count; if (frame_limit > 0 && frame_count >= frame_limit) { running = 0;