phantasia

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

commit 51b7345bc3712bb83e2a2f9e2e2b0efadee20a2d
parent 30803d85bd089d287bb0aa04aaaacbb42f814c65
Author: beep <beep@wimdupont.com>
Date:   Wed, 29 Jul 2026 13:58:04 +0000

Fix encounter contact and menu navigation

Diffstat:
Mphantasia.6 | 3++-
Msrc/engine/world.c | 2+-
Msrc/game/battle.c | 10+++++++---
Msrc/game/battle.h | 1+
Msrc/game/main.c | 59++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/tests/smoke.c | 18+++++++++++++++++-
6 files changed, 70 insertions(+), 23 deletions(-)

diff --git a/phantasia.6 b/phantasia.6 @@ -27,7 +27,8 @@ Move through inventory items, item actions, battle commands, and learned spells. .It Enter Open or confirm the selected inventory action, battle command, or spell. .It Escape -Return from the battle spell list to the command menu. +Return one level from an inventory or battle submenu, or close an open character +or top-level inventory window. .It Window close button Quit the game. .El diff --git a/src/engine/world.c b/src/engine/world.c @@ -142,7 +142,7 @@ ph_world_blocking_entity(const PhWorld *world, int self_index, PhVec2 pos) } def = ph_world_entity_def(world, entity->type_id); - if (!def || !def->blocks_movement) { + if (!def || (!def->blocks_movement && i != world->player_index)) { continue; } diff --git a/src/game/battle.c b/src/game/battle.c @@ -142,6 +142,12 @@ ph_battle_cast(PhBattle *battle, PhWorld *world) } void +ph_battle_cancel(PhBattle *battle) +{ + if (battle->phase == PH_BATTLE_MAGIC) battle->phase = PH_BATTLE_COMMAND; +} + +void ph_battle_order(const PhBattle *battle, const PhWorld *world, int order[PH_BATTLE_ORDER_COUNT]) { @@ -158,9 +164,7 @@ ph_battle_order(const PhBattle *battle, const PhWorld *world, if (i == 0 && actor == world->player_index) { if (battle->phase == PH_BATTLE_PLAYER_ACTION) delay = battle->action_delay; - else if (spell && (battle->phase == PH_BATTLE_MAGIC || - (battle->phase == PH_BATTLE_COMMAND && - battle->command == PH_BATTLE_MAGICK))) + else if (spell && battle->phase == PH_BATTLE_MAGIC) delay = delay * spell->delay_percent / 100; if (delay < 1) delay = 1; } diff --git a/src/game/battle.h b/src/game/battle.h @@ -47,6 +47,7 @@ typedef struct { void ph_battle_begin(PhBattle *battle, const PhWorld *world, int enemy_index); void ph_battle_select(PhBattle *battle, PhWorld *world); void ph_battle_cast(PhBattle *battle, PhWorld *world); +void ph_battle_cancel(PhBattle *battle); PhBattleResult ph_battle_update(PhBattle *battle, PhWorld *world, float dt, int transition); void ph_battle_order(const PhBattle *battle, const PhWorld *world, diff --git a/src/game/main.c b/src/game/main.c @@ -670,7 +670,7 @@ ph_draw_inventory(SDL_Renderer *renderer, const PhAssets *assets, ph_draw_inventory_popup(renderer, world, ui); } -static void +static int ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key) { int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 || @@ -678,6 +678,16 @@ ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key) 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_CANCEL) { + if (ui->mode == PH_INVENTORY_EXAMINE) + ui->mode = PH_INVENTORY_ACTIONS; + else if (ui->mode == PH_INVENTORY_ACTIONS) + ui->mode = PH_INVENTORY_BROWSE; + else + return 1; + ui->status[0] = '\0'; + return 0; + } if (key == PH_KEY_MENU_ACCEPT) { if (ui->mode == PH_INVENTORY_BROWSE && ui->selected >= 0) { int actions[PH_INVENTORY_ACTION_COUNT]; @@ -691,9 +701,9 @@ ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key) } else if (ui->mode == PH_INVENTORY_EXAMINE) { ui->mode = PH_INVENTORY_ACTIONS; } - return; + return 0; } - if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return; + if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return 0; if (ui->mode == PH_INVENTORY_ACTIONS) { ui->option = ph_inventory_action_step(&world->item_defs[ui->selected], ui->option, previous ? -1 : 1); @@ -701,6 +711,7 @@ ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key) ui->selected = ph_inventory_step(world, ui->selected, previous ? -1 : 1); ui->status[0] = '\0'; } + return 0; } static void @@ -746,14 +757,13 @@ ph_draw_battle(SDL_Renderer *renderer, const PhAssets *assets, const PhEntity *player = ph_world_player(world); const PhEntity *enemy = &world->entities[battle->enemy_index]; const PhEntityDef *enemy_def = ph_world_entity_def(world, enemy->type_id); - const PhSpellDef *spell = ph_battle_spell(battle); PhStats player_stats = ph_world_entity_stats(world, player); PhStats enemy_stats = ph_world_entity_stats(world, enemy); int order[PH_BATTLE_ORDER_COUNT]; SDL_FRect ground = { 0.0f, PH_VIEW_H - 136.0f, PH_VIEW_W, 72.0f }; SDL_FRect panel = { 8.0f, PH_VIEW_H - 64.0f, PH_VIEW_W - 16.0f, 56.0f }; - SDL_FRect spell_panel = { PH_VIEW_W * 0.5f - 64.0f, - PH_VIEW_H - 116.0f, 144.0f, + SDL_FRect spell_panel = { PH_VIEW_W * 0.5f - 88.0f, + PH_VIEW_H - 116.0f, 176.0f, 24.0f + (float)LEN(ph_player_spells) * 12.0f }; SDL_FRect enemy_rect = { 56.0f + ph_battle_lunge(battle, PH_BATTLE_ENEMY_ACTION), PH_VIEW_H - 172.0f, @@ -803,11 +813,6 @@ ph_draw_battle(SDL_Renderer *renderer, const PhAssets *assets, } SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); SDL_RenderDebugText(renderer, 104.0f, panel.y + 8.0f, battle->message); - if ((battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC) && - battle->command == PH_BATTLE_MAGICK && spell) { - snprintf(text, sizeof(text), "%s %dMP QUICK", spell->name, spell->mp_cost); - SDL_RenderDebugText(renderer, 104.0f, ph_ui_bottom(24.0f), text); - } snprintf(text, sizeof(text), "HP %d/%d", player->hp, player_stats.max_hp); SDL_RenderDebugText(renderer, ph_ui_right(80.0f), ph_ui_bottom(36.0f), text); snprintf(text, sizeof(text), "MP %d/%d", player->mp, player_stats.max_mp); @@ -824,8 +829,9 @@ ph_draw_battle(SDL_Renderer *renderer, const PhAssets *assets, const PhSpellDef *choice = ph_spell_def(ph_player_spells[i]); if (!choice) continue; - snprintf(text, sizeof(text), "%c %s %dMP", - battle->spell == i ? '>' : ' ', choice->name, choice->mp_cost); + snprintf(text, sizeof(text), "%c %s %dMP%s", + battle->spell == i ? '>' : ' ', choice->name, choice->mp_cost, + choice->delay_percent < 100 ? " QUICK" : ""); SDL_RenderDebugText(renderer, spell_panel.x + 8.0f, spell_panel.y + 20.0f + (float)i * 12.0f, text); } @@ -1055,6 +1061,15 @@ ph_run_render_smoke_test(void) fprintf(stderr, "render smoke test failed: inventory actions\n"); result = 1; } + inventory_ui.mode = PH_INVENTORY_EXAMINE; + if (ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_CANCEL) || + inventory_ui.mode != PH_INVENTORY_ACTIONS || + ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_CANCEL) || + inventory_ui.mode != PH_INVENTORY_BROWSE || + !ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_CANCEL)) { + fprintf(stderr, "render smoke test failed: inventory cancel\n"); + result = 1; + } ph_inventory_open(&inventory_ui, &world); for (i = 0; i < 10; ++i) { inventory_ui.mode = i == 8 ? PH_INVENTORY_EXAMINE : @@ -1105,8 +1120,8 @@ ph_run_render_smoke_test(void) } battle.command = PH_BATTLE_MAGICK; ph_battle_order(&battle, &world, order); - if (order[0] != world.player_index || order[1] != world.player_index) { - fprintf(stderr, "render smoke test failed: quick CTB order\n"); + if (order[0] != world.player_index || order[1] != enemy_index) { + fprintf(stderr, "render smoke test failed: command CTB order\n"); result = 1; } ph_battle_select(&battle, &world); @@ -1114,6 +1129,11 @@ ph_run_render_smoke_test(void) fprintf(stderr, "render smoke test failed: Magick submenu\n"); result = 1; } + ph_battle_order(&battle, &world, order); + if (order[0] != world.player_index || order[1] != world.player_index) { + fprintf(stderr, "render smoke test failed: spell CTB order\n"); + result = 1; + } ph_render_frame(renderer, &assets, &world, (PhInput){ 0 }, 0.0f, PH_MENU_NONE, &inventory_ui, &game, &battle); ph_battle_cast(&battle, &world); @@ -1230,7 +1250,7 @@ ph_run_game(int frame_limit) else if (key == PH_KEY_MENU_ACCEPT) ph_battle_cast(&battle, &world); else if (key == PH_KEY_MENU_CANCEL) - battle.phase = PH_BATTLE_COMMAND; + ph_battle_cancel(&battle); } else if (previous || next) { battle.command = (PhBattleCommand)((battle.command + (previous ? PH_BATTLE_COMMAND_COUNT - 1 : 1)) % PH_BATTLE_COMMAND_COUNT); @@ -1243,6 +1263,10 @@ ph_run_game(int frame_limit) game = PH_GAME_WORLD; menu = PH_MENU_NONE; } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && + game == PH_GAME_WORLD && menu == PH_MENU_CHARACTER && + event.key.scancode == PH_KEY_MENU_CANCEL) { + menu = PH_MENU_NONE; + } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && game == PH_GAME_WORLD && event.key.scancode == PH_KEY_CHARACTER) { menu = menu == PH_MENU_CHARACTER ? PH_MENU_NONE : PH_MENU_CHARACTER; } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && @@ -1255,7 +1279,8 @@ ph_run_game(int frame_limit) } } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && game == PH_GAME_WORLD && menu == PH_MENU_INVENTORY) { - ph_inventory_key(&inventory_ui, &world, event.key.scancode); + if (ph_inventory_key(&inventory_ui, &world, event.key.scancode)) + menu = PH_MENU_NONE; } } diff --git a/src/tests/smoke.c b/src/tests/smoke.c @@ -64,6 +64,13 @@ ph_movement_test(int open, int blocked) ph_world_tick(&world, (PhInput){ .move_x = 1 }, PH_MAX_FRAME_TIME); if (!world.entities[player_index].moving || world.entities[player_index].animation_time <= 0.0f) return 1; + world.entities[player_index].pos = player_start; + mob->pos = mob->move_target = (PhVec2){ + PH_TILE_CENTER(PLAYER_COL + 1), player_start.y }; + mob->moving = 0; + mob->move_credit = 0.0f; + ph_world_tick(&world, (PhInput){ 0 }, action_seconds); + if (world.encounter_index != mob_index) return 1; world.entities[player_index].pos = player_start; mob->pos = mob->move_target = mob_start; @@ -71,6 +78,7 @@ ph_movement_test(int open, int blocked) mob->moving = 0; mob->move_credit = 0.0f; mob->chase_turns = 2; + world.encounter_index = -1; tiles[ROW * area.width + WALL_COL] = (unsigned char)blocked; ph_world_tick(&world, (PhInput){ 0 }, action_seconds); if (mob->moving || mob->chase_turns != 1) return 1; @@ -172,11 +180,19 @@ ph_logic_test(int open, int blocked) ph_battle_order(&battle, &world, order); if (order[0] != player_index || order[1] != enemy_index) return 1; battle.command = PH_BATTLE_MAGICK; + ph_battle_order(&battle, &world, order); + if (order[0] != player_index || order[1] != enemy_index) return 1; ph_battle_select(&battle, &world); - if (battle.phase != PH_BATTLE_MAGIC || world.entities[player_index].mp != 5 || + ph_battle_order(&battle, &world, order); + if (battle.phase != PH_BATTLE_MAGIC || order[0] != player_index || + order[1] != player_index || world.entities[player_index].mp != 5 || world.encounter_index != enemy_index || ph_world_apply_accuracy_penalty(&world, enemy_index, 20, 3) < 0) return 1; + ph_battle_cancel(&battle); + ph_battle_order(&battle, &world, order); + if (battle.phase != PH_BATTLE_COMMAND || order[0] != player_index || + order[1] != enemy_index) return 1; ph_world_advance_effects(&world, enemy_index); ph_world_advance_effects(&world, enemy_index); ph_world_advance_effects(&world, enemy_index);