commit df6f8a604f475793cfaa0b12eef7f53d01c9a8cd
parent d1abfae121fdca02aba9e0d6d9133c21a7da1e94
Author: beep <beep@wimdupont.com>
Date: Wed, 29 Jul 2026 12:11:09 +0000
Add navigable inventory actions
Diffstat:
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;