phantasia

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

commit 22c1f0882a2850f569b6c94299736cf2f99162ef
parent 7dbf0b5d655f5c5d749a9550c7f08c03b13a68fb
Author: beep <beep@wimdupont.com>
Date:   Wed, 29 Jul 2026 13:25:42 +0000

Refactor core game modules and configuration

Diffstat:
MMakefile | 6++++--
MREADME.adoc | 19++++++++++++++-----
Mconfig.def.h | 162+++++--------------------------------------------------------------------------
Msrc/engine/world.c | 38+++++++++++++++-----------------------
Msrc/engine/world.h | 16+++-------------
Asrc/game/battle.c | 265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/game/battle.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/game/content.c | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/game/content.h | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/game/main.c | 481+++++++++++++++++++++++--------------------------------------------------------
Msrc/tools/mapc.c | 1+
11 files changed, 701 insertions(+), 536 deletions(-)

diff --git a/Makefile b/Makefile @@ -15,9 +15,11 @@ CRITTERS_BMP = $(OBJDIR)/assets/critters.bmp SRC = \ src/engine/world.c \ + src/game/battle.c \ + src/game/content.c \ src/game/main.c -MAPC_SRC = src/tools/mapc.c +MAPC_SRC = src/tools/mapc.c src/game/content.c CONFIG = config.h OBJ = $(SRC:src/%.c=$(OBJDIR)/%.o) @@ -48,7 +50,7 @@ $(BIN): $(OBJ) $(MAPBIN) $(TILESET_BMP) $(HERO_BMP) $(ITEMS_BMP) $(CRITTERS_BMP) $(MAPC): $(MAPC_SRC) $(CONFIG) @mkdir -p $(dir $@) - $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@ + $(CC) $(CPPFLAGS) $(CFLAGS) $(MAPC_SRC) -o $@ $(OBJDIR)/%.o: src/%.c @mkdir -p $(dir $@) diff --git a/README.adoc b/README.adoc @@ -11,6 +11,10 @@ * Story should stay optional and discoverable instead of interrupting gameplay. * Keep most systems generic and engine-like so monsters, NPCs, areas, and loot can be expanded without hardcoding one-off cases. +Core world and CTB battle logic are independent of SDL. `src/game/main.c` +composes those systems with input and presentation; `src/game/battle.c` owns the +battle state machine and can be exercised by the headless build. + == Principles Phantasia's gameplay and implementation may change substantially while it is @@ -48,11 +52,16 @@ When the configuration API version changes, an older `config.h` is rejected at compile time with a clear error. Merge the new fields from `config.def.h`, or replace `config.h` with it if no local customizations need to be retained. -Configuration currently owns the initial world definitions, entity spawns, -item drops, display dimensions, asset paths and color keys, map tile choices, -starting equipment, learned spells, CTB timing and spell tuning, rewards, and controls. -Adding content should normally extend these configured arrays without adding -another special case to the core initialization code. +Configuration owns compile-time settings such as display dimensions, engine +radii and timing, paths, colors, and controls. Concrete game content—assets, +tiles, entities, items, spells, starting equipment, spawns, drops, and rewards— +lives in the typed `src/game/content.c`/`content.h` module. Adding content should +extend those data tables without adding renderer or initialization special cases. + +The SDL window is runtime-resizable. Rendering stays on the configured logical +canvas and uses integer presentation scaling; right-, bottom-, and center-aligned +interface elements are derived from that canvas rather than a fixed 320x240 +desktop window size. == Build diff --git a/config.def.h b/config.def.h @@ -1,7 +1,7 @@ #ifndef CONFIG_H #define CONFIG_H -#define PH_CONFIG_VERSION 2 +#define PH_CONFIG_VERSION 5 #define PH_VIEW_W 320 #define PH_VIEW_H 240 @@ -16,11 +16,15 @@ #define PH_BATTLE_TRANSITION_SECONDS 0.55f #define PH_BATTLE_ACTION_SECONDS 0.50f #define PH_BATTLE_CTB_BASE 1000 -#define PH_FLASH_MP_COST 15 -#define PH_FLASH_POWER 5 -#define PH_FLASH_DELAY_PERCENT 50 -#define PH_FLASH_ACCURACY_PENALTY 20 -#define PH_FLASH_ACCURACY_TURNS 3 +#define PH_BATTLE_RESOLVE_PERCENT 50 +#define PH_BATTLE_LUNGE_DISTANCE 20.0f + +#define PH_HIT_CHANCE_MIN 5 +#define PH_HIT_CHANCE_MAX 100 +#define PH_ACCURACY_LUCK_DIVISOR 2 +#define PH_DAMAGE_LUCK_DIVISOR 4 +#define PH_PHYSICAL_DEFENSE_DIVISOR 2 +#define PH_MAGIC_DEFENSE_DIVISOR 2 #define PH_START_MAP_PATH "obj/maps/ashen-meadow.phmap" #define PH_RENDER_SMOKE_PATH "obj/render-smoke.bmp" @@ -39,152 +43,6 @@ #define PH_BATTLE_BG_COLOR 44, 52, 68, 255 #define PH_BATTLE_GROUND_COLOR 72, 84, 82, 255 -enum { - PH_ASSET_NONE, - PH_ASSET_TILESET, - PH_ASSET_HERO, - PH_ASSET_ITEMS, - PH_ASSET_CRITTERS, -}; - -static const struct { - const char *path; - unsigned char key_r, key_g, key_b; -} ph_asset_defs[] = { - [PH_ASSET_TILESET] = { "obj/assets/overworld_tileset_grass.bmp", 0, 0, 0 }, - [PH_ASSET_HERO] = { "obj/assets/hero.bmp", 0, 0, 0 }, - [PH_ASSET_ITEMS] = { "obj/assets/items.bmp", 111, 119, 109 }, - [PH_ASSET_CRITTERS] = { "obj/assets/critters.bmp", 255, 147, 153 }, -}; - -static const PhTileDef ph_tile_defs[] = { - { '.', 0, PH_ASSET_TILESET, 1, 1 }, - { '#', 1, PH_ASSET_TILESET, 1, 5 }, -}; - -enum { - PH_DEF_PLAYER = 1, - PH_DEF_SLIME, -}; - -enum { - PH_ITEM_RUSTED_SPEAR = 1, - PH_ITEM_WAYFARER_BLADE, - PH_ITEM_TRAVELER_COAT, - PH_ITEM_HEALING_POTION, -}; - -enum { PH_SPELL_FLASH = 1 }; - -static const PhSpellDef ph_spell_defs[] = { - { PH_SPELL_FLASH, "Flash", PH_FLASH_MP_COST, PH_FLASH_POWER, - PH_FLASH_DELAY_PERCENT, PH_FLASH_ACCURACY_PENALTY, - PH_FLASH_ACCURACY_TURNS }, -}; - -static const int ph_player_spells[] = { PH_SPELL_FLASH }; - -static const PhEntityDef ph_entity_defs[] = { - { - .id = PH_DEF_PLAYER, - .name = "Wayfarer", - .stats = { .max_hp = 40, .max_mp = 50, .strength = 5, .defense = 3, - .magic = 4, .magic_defense = 3, .agility = 10, .luck = 5, - .evasion = 2, .accuracy = 100 }, - .move_speed = 90, - .asset_id = PH_ASSET_HERO, - .sprite_tiles = { { 4, 0 }, { 0, 0 }, { 0, 1 } }, - .blocks_movement = 0, - .kind = PH_ENTITY_PLAYER, - }, - { - .id = PH_DEF_SLIME, - .name = "Mire Slime", - .stats = { .max_hp = 12, .strength = 3, .defense = 1, - .magic_defense = 1, .agility = 10, .luck = 1, - .evasion = 2, .accuracy = 100 }, - .move_speed = 45, - .xp_reward = 8, - .loot_item_id = PH_ITEM_HEALING_POTION, - .loot_amount = 1, - .asset_id = PH_ASSET_CRITTERS, - .sprite_tiles = { { 12, 0 }, { 12, 0 }, { 12, 0 } }, - .blocks_movement = 1, - .kind = PH_ENTITY_MONSTER, - }, -}; - -static const PhItemDef ph_item_defs[] = { - { - .id = PH_ITEM_RUSTED_SPEAR, - .name = "Rusted Spear", - .description = "A worn but sturdy spear.", - .value = 12, - .bonuses = { .strength = 2 }, - .equip_slot = PH_EQUIP_WEAPON, - .asset_id = PH_ASSET_ITEMS, - .sprite_tile_x = 2, - .sprite_tile_y = 6, - }, - { - .id = PH_ITEM_WAYFARER_BLADE, - .name = "Wayfarer Blade", - .description = "A reliable traveler's blade.", - .value = 20, - .bonuses = { .strength = 3 }, - .equip_slot = PH_EQUIP_WEAPON, - .asset_id = PH_ASSET_ITEMS, - .sprite_tile_x = 0, - .sprite_tile_y = 0, - }, - { - .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, - .asset_id = PH_ASSET_ITEMS, - .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] = { - [PH_EQUIP_WEAPON] = PH_ITEM_WAYFARER_BLADE, - [PH_EQUIP_ARMOR] = PH_ITEM_TRAVELER_COAT, -}; - -static const struct { - int type_id; - PhVec2 pos; -} ph_entity_spawns[] = { - { PH_DEF_PLAYER, { 80.0f, 80.0f } }, - { PH_DEF_SLIME, { 180.0f, 120.0f } }, -}; - -#define PH_PLAYER_SPAWN 0 - -static const struct { - int item_id; - PhVec2 pos; - 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 #define PH_KEY_LEFT_1 SDL_SCANCODE_LEFT #define PH_KEY_LEFT_2 SDL_SCANCODE_A diff --git a/src/engine/world.c b/src/engine/world.c @@ -7,7 +7,10 @@ #include <math.h> #include <string.h> -#define LEN(a) (sizeof(a) / sizeof(0[a])) +#if PH_ACCURACY_LUCK_DIVISOR <= 0 || PH_DAMAGE_LUCK_DIVISOR <= 0 || \ + PH_PHYSICAL_DEFENSE_DIVISOR <= 0 || PH_MAGIC_DEFENSE_DIVISOR <= 0 +#error "combat formula divisors must be positive" +#endif typedef struct { uint32_t magic; @@ -44,16 +47,6 @@ ph_clampi(int value, int min, int max) return value; } -static PhStats -ph_canonical_stats(PhStats stats) -{ - stats.strength += stats.attack; - stats.agility += stats.speed; - stats.attack = stats.strength; - stats.speed = stats.agility; - return stats; -} - static unsigned int ph_world_random(PhWorld *world) { @@ -345,8 +338,8 @@ ph_world_spawn_entity(PhWorld *world, int type_id, PhVec2 pos) memset(entity, 0, sizeof(*entity)); entity->type_id = type_id; entity->pos = pos; - entity->hp = ph_canonical_stats(def->stats).max_hp; - entity->mp = ph_canonical_stats(def->stats).max_mp; + entity->hp = def->stats.max_hp; + entity->mp = def->stats.max_mp; entity->facing_x = 0; entity->facing_y = 1; entity->active = 1; @@ -495,9 +488,11 @@ ph_world_physical_attack(PhWorld *world, int attacker_index, int target_index) defense = ph_world_entity_stats(world, target); accuracy = attack.accuracy * (100 - ph_clampi(attacker->accuracy_penalty, 0, 100)) / 100; - chance = ph_clampi(accuracy + attack.luck / 2 - defense.evasion, 5, 100); + chance = ph_clampi(accuracy + attack.luck / PH_ACCURACY_LUCK_DIVISOR - + defense.evasion, PH_HIT_CHANCE_MIN, PH_HIT_CHANCE_MAX); if ((int)(ph_world_random(world) % 100u) >= chance) return 0; - damage = attack.strength + attack.luck / 4 - defense.defense / 2; + damage = attack.strength + attack.luck / PH_DAMAGE_LUCK_DIVISOR - + defense.defense / PH_PHYSICAL_DEFENSE_DIVISOR; damage = damage > 0 ? damage : 1; target->hp = ph_clampi(target->hp - damage, 0, defense.max_hp); return damage; @@ -547,7 +542,7 @@ ph_world_magic_attack(PhWorld *world, int attacker_index, int target_index, target->hp <= 0 || attacker->mp < mp_cost) return -1; defense = ph_world_entity_stats(world, target); damage = ph_world_entity_stats(world, attacker).magic + power - - defense.magic_defense / 2; + defense.magic_defense / PH_MAGIC_DEFENSE_DIVISOR; damage = damage > 0 ? damage : 1; attacker->mp -= mp_cost; target->hp = ph_clampi(target->hp - damage, 0, defense.max_hp); @@ -608,8 +603,7 @@ ph_world_tick(PhWorld *world, PhInput input, float dt) input = ph_orthogonal_input(input); next = player->pos; if (input.move_x != 0 || input.move_y != 0) { - float speed = (float)(def->move_speed > 0 ? def->move_speed : - ph_world_entity_stats(world, player).speed); + float speed = (float)def->move_speed; player->facing_x = input.move_x; player->facing_y = input.move_y; @@ -671,13 +665,13 @@ ph_world_entity_stats(const PhWorld *world, const PhEntity *entity) int slot; if (!entity || !(def = ph_world_entity_def(world, entity->type_id))) return stats; - stats = ph_canonical_stats(def->stats); + stats = def->stats; for (slot = PH_EQUIP_WEAPON; slot < PH_EQUIP_COUNT; ++slot) { const PhItemDef *item = ph_world_item_def(world, entity->equipment[slot]); PhStats bonus; if (!item) continue; - bonus = ph_canonical_stats(item->bonuses); + bonus = item->bonuses; stats.max_hp += bonus.max_hp; stats.max_mp += bonus.max_mp; stats.strength += bonus.strength; @@ -689,8 +683,6 @@ ph_world_entity_stats(const PhWorld *world, const PhEntity *entity) stats.evasion += bonus.evasion; stats.accuracy += bonus.accuracy; } - stats.attack = stats.strength; - stats.speed = stats.agility; return stats; } @@ -703,7 +695,7 @@ ph_area_tile_def(const PhArea *area, int tx, int ty) return NULL; } tile = area->tiles[(size_t)ty * (size_t)area->width + (size_t)tx]; - return tile < LEN(ph_tile_defs) ? &ph_tile_defs[tile] : NULL; + return tile < area->tile_def_count ? &area->tile_defs[tile] : NULL; } int diff --git a/src/engine/world.h b/src/engine/world.h @@ -3,7 +3,7 @@ #include <stddef.h> -#define PH_CONFIG_API_VERSION 2 +#define PH_CONFIG_API_VERSION 5 enum { PH_MAP_MAGIC = 0x50484d50, @@ -52,12 +52,10 @@ typedef struct { int max_hp; int max_mp; int strength; - int attack; /* legacy config alias folded into strength */ int defense; int magic; int magic_defense; int agility; - int speed; /* legacy config alias folded into agility */ int luck; int evasion; int accuracy; @@ -100,16 +98,6 @@ typedef struct { } PhItemDef; typedef struct { - int id; - const char *name; - int mp_cost; - int power; - int delay_percent; - int accuracy_penalty; - int accuracy_turns; -} PhSpellDef; - -typedef struct { int type_id; PhVec2 pos; int hp; @@ -136,6 +124,8 @@ typedef struct { int height; unsigned char *tiles; int owns_tiles; + const PhTileDef *tile_defs; + int tile_def_count; } PhArea; typedef struct { diff --git a/src/game/battle.c b/src/game/battle.c @@ -0,0 +1,265 @@ +#include "game/battle.h" + +#include <stdio.h> +#include <string.h> + +#include "config.h" + +#define LEN(a) (sizeof(a) / sizeof(0[a])) + +#if PH_BATTLE_CTB_BASE <= 0 || PH_BATTLE_RESOLVE_PERCENT < 0 || \ + PH_BATTLE_RESOLVE_PERCENT > 100 +#error "invalid CTB configuration" +#endif + +static int +ph_battle_actor_delay(const PhWorld *world, int actor) +{ + PhStats stats = ph_world_entity_stats(world, &world->entities[actor]); + int delay = PH_BATTLE_CTB_BASE / (stats.agility > 0 ? stats.agility : 1); + + return delay > 0 ? delay : 1; +} + +static void +ph_battle_start_turn(PhBattle *battle, const PhWorld *world, int actor) +{ + battle->current_actor = actor; + battle->action_delay = ph_battle_actor_delay(world, actor); + battle->resolved = 0; + if (actor == world->player_index) { + battle->phase = PH_BATTLE_COMMAND; + battle->timer = 0.0f; + } else { + battle->phase = PH_BATTLE_ENEMY_ACTION; + battle->timer = PH_BATTLE_ACTION_SECONDS; + } +} + +static void +ph_battle_next_turn(PhBattle *battle, const PhWorld *world) +{ + int previous = battle->current_actor; + int next; + + if (previous == world->player_index) battle->player_ctb += battle->action_delay; + else battle->enemy_ctb += battle->action_delay; + if (battle->player_ctb < battle->enemy_ctb) next = world->player_index; + else if (battle->enemy_ctb < battle->player_ctb) next = battle->enemy_index; + else next = previous == world->player_index ? battle->enemy_index : world->player_index; + ph_battle_start_turn(battle, world, next); +} + +static int +ph_battle_item(const PhWorld *world) +{ + const PhEntity *player = ph_world_player(world); + PhStats stats = ph_world_entity_stats(world, player); + int i; + + for (i = 0; i < world->item_def_count; ++i) + if (world->inventory[i] > 0 && world->item_defs[i].use != PH_ITEM_USE_NONE && + player && player->hp < stats.max_hp) return world->item_defs[i].id; + return 0; +} + +const PhSpellDef * +ph_spell_def(int id) +{ + size_t i; + + for (i = 0; i < LEN(ph_spell_defs); ++i) + if (ph_spell_defs[i].id == id) return &ph_spell_defs[i]; + return NULL; +} + +const PhSpellDef * +ph_battle_spell(const PhBattle *battle) +{ + if (battle->spell < 0 || (size_t)battle->spell >= LEN(ph_player_spells)) + return NULL; + return ph_spell_def(ph_player_spells[battle->spell]); +} + +void +ph_battle_begin(PhBattle *battle, const PhWorld *world, int enemy_index) +{ + const PhEntity *enemy = &world->entities[enemy_index]; + const PhEntityDef *def = ph_world_entity_def(world, enemy->type_id); + + memset(battle, 0, sizeof(*battle)); + battle->enemy_index = enemy_index; + battle->current_actor = world->player_index; + battle->enemy_ctb = ph_battle_actor_delay(world, enemy_index); + battle->timer = PH_BATTLE_TRANSITION_SECONDS; + if (def) { + battle->xp_reward = def->xp_reward; + battle->loot_item_id = def->loot_item_id; + battle->loot_amount = def->loot_amount; + snprintf(battle->message, sizeof(battle->message), "%s approaches!", def->name); + } +} + +void +ph_battle_select(PhBattle *battle, PhWorld *world) +{ + if (battle->phase != PH_BATTLE_COMMAND || !ph_world_player(world) || + battle->current_actor != world->player_index) return; + if (battle->command == PH_BATTLE_MAGICK) { + battle->spell = 0; + battle->phase = PH_BATTLE_MAGIC; + snprintf(battle->message, sizeof(battle->message), "Choose a spell."); + return; + } + if (battle->command == PH_BATTLE_ITEM && + !(battle->item_id = ph_battle_item(world))) { + snprintf(battle->message, sizeof(battle->message), "No usable item."); + return; + } + battle->phase = PH_BATTLE_PLAYER_ACTION; + battle->timer = PH_BATTLE_ACTION_SECONDS; + battle->resolved = 0; + battle->action_delay = ph_battle_actor_delay(world, world->player_index); +} + +void +ph_battle_cast(PhBattle *battle, PhWorld *world) +{ + const PhEntity *player = ph_world_player(world); + const PhSpellDef *spell = ph_battle_spell(battle); + + if (battle->phase != PH_BATTLE_MAGIC || !player || !spell) return; + if (player->mp < spell->mp_cost) { + snprintf(battle->message, sizeof(battle->message), "Not enough MP."); + return; + } + battle->phase = PH_BATTLE_PLAYER_ACTION; + battle->timer = PH_BATTLE_ACTION_SECONDS; + battle->resolved = 0; + battle->action_delay = ph_battle_actor_delay(world, world->player_index) * + spell->delay_percent / 100; + if (battle->action_delay < 1) battle->action_delay = 1; +} + +void +ph_battle_order(const PhBattle *battle, const PhWorld *world, + int order[PH_BATTLE_ORDER_COUNT]) +{ + int player_ctb = battle->player_ctb; + int enemy_ctb = battle->enemy_ctb; + int actor = battle->current_actor; + int i; + + for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) { + int delay = ph_battle_actor_delay(world, actor); + const PhSpellDef *spell = ph_battle_spell(battle); + + order[i] = actor; + 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))) + delay = delay * spell->delay_percent / 100; + if (delay < 1) delay = 1; + } + if (actor == world->player_index) player_ctb += delay; + else enemy_ctb += delay; + if (player_ctb < enemy_ctb) actor = world->player_index; + else if (enemy_ctb < player_ctb) actor = battle->enemy_index; + else actor = actor == world->player_index ? + battle->enemy_index : world->player_index; + } +} + +static void +ph_battle_resolve(PhBattle *battle, PhWorld *world) +{ + const PhEntityDef *def; + const PhSpellDef *spell = NULL; + int damage; + + if (battle->phase == PH_BATTLE_PLAYER_ACTION) { + def = ph_world_entity_def(world, world->entities[battle->enemy_index].type_id); + if (battle->command == PH_BATTLE_FIGHT) { + damage = ph_world_physical_attack(world, world->player_index, + battle->enemy_index); + } else if (battle->command == PH_BATTLE_MAGICK) { + spell = ph_battle_spell(battle); + if (!spell) return; + damage = ph_world_magic_attack(world, world->player_index, + battle->enemy_index, spell->mp_cost, spell->power); + if (damage >= 0 && spell->accuracy_penalty > 0 && + spell->accuracy_turns > 0) + ph_world_apply_accuracy_penalty(world, battle->enemy_index, + spell->accuracy_penalty, spell->accuracy_turns); + } else { + const PhItemDef *item = ph_world_item_def(world, battle->item_id); + + damage = ph_world_use_inventory_item(world, world->player_index, + battle->item_id) < 0 ? -1 : 0; + snprintf(battle->message, sizeof(battle->message), "Used %s.", + item && item->name ? item->name : "item"); + return; + } + if (battle->command == PH_BATTLE_MAGICK && damage >= 0) + snprintf(battle->message, sizeof(battle->message), + "%s deals %d damage.", spell->name, damage); + else + snprintf(battle->message, sizeof(battle->message), damage > 0 ? + "%s takes %d damage." : "The attack misses.", + def && def->name ? def->name : "Enemy", damage); + return; + } + + def = ph_world_entity_def(world, world->entities[battle->enemy_index].type_id); + damage = ph_world_physical_attack(world, battle->enemy_index, world->player_index); + ph_world_advance_effects(world, battle->enemy_index); + snprintf(battle->message, sizeof(battle->message), damage > 0 ? + "%s deals %d damage." : "%s misses.", + def && def->name ? def->name : "Enemy", damage); +} + +PhBattleResult +ph_battle_update(PhBattle *battle, PhWorld *world, float dt, int transition) +{ + if (transition) { + battle->timer -= dt; + if (battle->timer > 0.0f) return PH_BATTLE_CONTINUE; + battle->timer = 0.0f; + ph_battle_start_turn(battle, world, world->player_index); + return PH_BATTLE_READY; + } + if (battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC) + return PH_BATTLE_CONTINUE; + battle->timer -= dt; + if (!battle->resolved && battle->timer <= PH_BATTLE_ACTION_SECONDS * + PH_BATTLE_RESOLVE_PERCENT / 100.0f) { + ph_battle_resolve(battle, world); + battle->resolved = 1; + } + if (battle->timer > 0.0f) return PH_BATTLE_CONTINUE; + if (battle->phase == PH_BATTLE_PLAYER_ACTION && + world->entities[battle->enemy_index].hp <= 0) { + if (ph_world_finish_encounter(world, battle->enemy_index) == 0) + return PH_BATTLE_VICTORY; + return PH_BATTLE_CONTINUE; + } + if (battle->phase == PH_BATTLE_ENEMY_ACTION && + world->entities[world->player_index].hp <= 0) + return PH_BATTLE_DEFEAT; + ph_battle_next_turn(battle, world); + return PH_BATTLE_CONTINUE; +} + +float +ph_battle_lunge(const PhBattle *battle, PhBattlePhase phase) +{ + float progress; + + if (battle->phase != phase || PH_BATTLE_ACTION_SECONDS <= 0.0f) return 0.0f; + progress = 1.0f - battle->timer / PH_BATTLE_ACTION_SECONDS; + return (progress < 0.5f ? progress : 1.0f - progress) * + PH_BATTLE_LUNGE_DISTANCE; +} diff --git a/src/game/battle.h b/src/game/battle.h @@ -0,0 +1,58 @@ +#ifndef PH_GAME_BATTLE_H +#define PH_GAME_BATTLE_H + +#include "game/content.h" + +enum { PH_BATTLE_ORDER_COUNT = 6 }; + +typedef enum { + PH_BATTLE_COMMAND, + PH_BATTLE_MAGIC, + PH_BATTLE_PLAYER_ACTION, + PH_BATTLE_ENEMY_ACTION, +} PhBattlePhase; + +typedef enum { + PH_BATTLE_CONTINUE, + PH_BATTLE_READY, + PH_BATTLE_VICTORY, + PH_BATTLE_DEFEAT, +} PhBattleResult; + +typedef enum { + PH_BATTLE_FIGHT, + PH_BATTLE_MAGICK, + PH_BATTLE_ITEM, + PH_BATTLE_COMMAND_COUNT, +} PhBattleCommand; + +typedef struct { + int enemy_index; + PhBattleCommand command; + int spell; + int item_id; + int resolved; + int xp_reward; + int loot_item_id; + int loot_amount; + int player_ctb; + int enemy_ctb; + int current_actor; + int action_delay; + float timer; + PhBattlePhase phase; + char message[64]; +} PhBattle; + +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); +PhBattleResult ph_battle_update(PhBattle *battle, PhWorld *world, float dt, + int transition); +void ph_battle_order(const PhBattle *battle, const PhWorld *world, + int order[PH_BATTLE_ORDER_COUNT]); +const PhSpellDef *ph_spell_def(int id); +const PhSpellDef *ph_battle_spell(const PhBattle *battle); +float ph_battle_lunge(const PhBattle *battle, PhBattlePhase phase); + +#endif diff --git a/src/game/content.c b/src/game/content.c @@ -0,0 +1,111 @@ +#include "game/content.h" + +const PhAssetDef ph_asset_defs[PH_ASSET_COUNT] = { + [PH_ASSET_TILESET] = { "obj/assets/overworld_tileset_grass.bmp", 0, 0, 0 }, + [PH_ASSET_HERO] = { "obj/assets/hero.bmp", 0, 0, 0 }, + [PH_ASSET_ITEMS] = { "obj/assets/items.bmp", 111, 119, 109 }, + [PH_ASSET_CRITTERS] = { "obj/assets/critters.bmp", 255, 147, 153 }, +}; + +const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = { + { '.', 0, PH_ASSET_TILESET, 1, 1 }, + { '#', 1, PH_ASSET_TILESET, 1, 5 }, +}; + +const PhSpellDef ph_spell_defs[PH_SPELL_DEF_COUNT] = { + { PH_SPELL_FLASH, "Flash", 15, 5, 50, 20, 3 }, +}; + +const int ph_player_spells[PH_PLAYER_SPELL_COUNT] = { PH_SPELL_FLASH }; + +const PhEntityDef ph_entity_defs[PH_ENTITY_DEF_COUNT] = { + { + .id = PH_DEF_PLAYER, + .name = "Wayfarer", + .stats = { .max_hp = 40, .max_mp = 50, .strength = 5, .defense = 3, + .magic = 4, .magic_defense = 3, .agility = 10, .luck = 5, + .evasion = 2, .accuracy = 100 }, + .move_speed = 90, + .asset_id = PH_ASSET_HERO, + .sprite_tiles = { { 4, 0 }, { 0, 0 }, { 0, 1 } }, + .blocks_movement = 0, + .kind = PH_ENTITY_PLAYER, + }, + { + .id = PH_DEF_SLIME, + .name = "Mire Slime", + .stats = { .max_hp = 12, .strength = 3, .defense = 1, + .magic_defense = 1, .agility = 10, .luck = 1, + .evasion = 2, .accuracy = 100 }, + .move_speed = 45, + .xp_reward = 8, + .loot_item_id = PH_ITEM_HEALING_POTION, + .loot_amount = 1, + .asset_id = PH_ASSET_CRITTERS, + .sprite_tiles = { { 12, 0 }, { 12, 0 }, { 12, 0 } }, + .blocks_movement = 1, + .kind = PH_ENTITY_MONSTER, + }, +}; + +const PhItemDef ph_item_defs[PH_ITEM_DEF_COUNT] = { + { + .id = PH_ITEM_RUSTED_SPEAR, + .name = "Rusted Spear", + .description = "A worn but sturdy spear.", + .value = 12, + .bonuses = { .strength = 2 }, + .equip_slot = PH_EQUIP_WEAPON, + .asset_id = PH_ASSET_ITEMS, + .sprite_tile_x = 2, + .sprite_tile_y = 6, + }, + { + .id = PH_ITEM_WAYFARER_BLADE, + .name = "Wayfarer Blade", + .description = "A reliable traveler's blade.", + .value = 20, + .bonuses = { .strength = 3 }, + .equip_slot = PH_EQUIP_WEAPON, + .asset_id = PH_ASSET_ITEMS, + .sprite_tile_x = 0, + .sprite_tile_y = 0, + }, + { + .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, + .asset_id = PH_ASSET_ITEMS, + .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, + }, +}; + +const int ph_player_equipment[PH_EQUIP_COUNT] = { + [PH_EQUIP_WEAPON] = PH_ITEM_WAYFARER_BLADE, + [PH_EQUIP_ARMOR] = PH_ITEM_TRAVELER_COAT, +}; + +const PhEntitySpawn ph_entity_spawns[PH_ENTITY_SPAWN_COUNT] = { + { PH_DEF_PLAYER, { 80.0f, 80.0f } }, + { PH_DEF_SLIME, { 180.0f, 120.0f } }, +}; + +const PhItemDrop ph_item_drops[PH_ITEM_DROP_COUNT] = { + { PH_ITEM_RUSTED_SPEAR, { 104.0f, 80.0f }, 1 }, + { PH_ITEM_HEALING_POTION, { 104.0f, 96.0f }, 1 }, +}; diff --git a/src/game/content.h b/src/game/content.h @@ -0,0 +1,80 @@ +#ifndef PH_GAME_CONTENT_H +#define PH_GAME_CONTENT_H + +#include "engine/world.h" + +typedef struct { + const char *path; + unsigned char key_r; + unsigned char key_g; + unsigned char key_b; +} PhAssetDef; + +typedef struct { + int type_id; + PhVec2 pos; +} PhEntitySpawn; + +typedef struct { + int item_id; + PhVec2 pos; + int amount; +} PhItemDrop; + +typedef struct { + int id; + const char *name; + int mp_cost; + int power; + int delay_percent; + int accuracy_penalty; + int accuracy_turns; +} PhSpellDef; + +enum { + PH_ASSET_NONE, + PH_ASSET_TILESET, + PH_ASSET_HERO, + PH_ASSET_ITEMS, + PH_ASSET_CRITTERS, + PH_ASSET_COUNT, +}; + +enum { + PH_DEF_PLAYER = 1, + PH_DEF_SLIME, + PH_ENTITY_DEF_COUNT = 2, +}; + +enum { + PH_ITEM_RUSTED_SPEAR = 1, + PH_ITEM_WAYFARER_BLADE, + PH_ITEM_TRAVELER_COAT, + PH_ITEM_HEALING_POTION, + PH_ITEM_DEF_COUNT = 4, +}; + +enum { + PH_SPELL_FLASH = 1, + PH_SPELL_DEF_COUNT = 1, + PH_PLAYER_SPELL_COUNT = 1, +}; + +enum { + PH_TILE_DEF_COUNT = 2, + PH_ENTITY_SPAWN_COUNT = 2, + PH_ITEM_DROP_COUNT = 2, + PH_PLAYER_SPAWN = 0, +}; + +extern const PhAssetDef ph_asset_defs[PH_ASSET_COUNT]; +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 int ph_player_equipment[PH_EQUIP_COUNT]; +extern const PhEntitySpawn ph_entity_spawns[PH_ENTITY_SPAWN_COUNT]; +extern const PhItemDrop ph_item_drops[PH_ITEM_DROP_COUNT]; + +#endif diff --git a/src/game/main.c b/src/game/main.c @@ -1,4 +1,6 @@ #include "engine/world.h" +#include "game/battle.h" +#include "game/content.h" #include <stdbool.h> #include <stdio.h> @@ -38,33 +40,6 @@ typedef enum { } PhGameMode; typedef enum { - PH_BATTLE_COMMAND, - PH_BATTLE_MAGIC, - PH_BATTLE_PLAYER_ACTION, - PH_BATTLE_ENEMY_ACTION, -} PhBattlePhase; - -enum { PH_BATTLE_ORDER_COUNT = 6 }; - -typedef struct { - int enemy_index; - int command; - int spell; - int item_id; - int resolved; - int xp_reward; - int loot_item_id; - int loot_amount; - int player_ctb; - int enemy_ctb; - int current_actor; - int action_delay; - float timer; - PhBattlePhase phase; - char message[64]; -} PhBattle; - -typedef enum { PH_INVENTORY_BROWSE, PH_INVENTORY_ACTIONS, PH_INVENTORY_EXAMINE, @@ -84,6 +59,26 @@ typedef struct { PhInventoryMode mode; char status[64]; } PhInventoryUi; + +enum { PH_UI_FONT_W = 8 }; + +static float +ph_ui_right(float inset) +{ + return (float)PH_VIEW_W - inset; +} + +static float +ph_ui_bottom(float inset) +{ + return (float)PH_VIEW_H - inset; +} + +static float +ph_ui_center_text(const char *text) +{ + return ((float)PH_VIEW_W - (float)strlen(text) * PH_UI_FONT_W) * 0.5f; +} #endif #if PH_USE_SDL @@ -99,6 +94,8 @@ ph_make_world(PhWorld *world) fprintf(stderr, "failed to load %s\n", PH_START_MAP_PATH); return -1; } + area.tile_defs = ph_tile_defs; + area.tile_def_count = (int)LEN(ph_tile_defs); ph_world_init(world, area, (float)PH_VIEW_W, (float)PH_VIEW_H); for (i = 0; i < LEN(ph_entity_defs); ++i) @@ -135,10 +132,15 @@ static int ph_run_smoke_test(void) { unsigned char tiles[12]; - PhArea area = { "Smoke", 4, 3, tiles, 0 }; + PhArea area = { + .name = "Smoke", .width = 4, .height = 3, .tiles = tiles, + .tile_defs = ph_tile_defs, .tile_def_count = (int)LEN(ph_tile_defs), + }; PhWorld world; + PhBattle battle; const PhEntity *player; PhStats stats; + int order[PH_BATTLE_ORDER_COUNT]; int open = -1; int blocked = -1; size_t tile; @@ -159,7 +161,8 @@ ph_run_smoke_test(void) ph_world_init(&world, area, 64.0f, 48.0f); if (ph_world_add_entity_def(&world, (PhEntityDef){ .id = 1, .name = "Tester", .stats = { .max_hp = 5, .max_mp = 5, - .strength = 4, .magic = 3, .accuracy = 100, .speed = 1 }, + .strength = 4, .magic = 3, .accuracy = 100, .agility = 1 }, + .move_speed = 1, .kind = PH_ENTITY_PLAYER }) < 0 || ph_world_add_entity_def(&world, (PhEntityDef){ .id = 4, .name = "Target", .stats = { .max_hp = 8, .strength = 2, .defense = 1, @@ -167,7 +170,7 @@ ph_run_smoke_test(void) .loot_amount = 1, .blocks_movement = 1, .kind = PH_ENTITY_MONSTER }) < 0 || ph_world_add_item_def(&world, (PhItemDef){ - .id = 1, .bonuses = { .max_hp = 2, .attack = 2 }, + .id = 1, .bonuses = { .max_hp = 2, .strength = 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 || @@ -211,7 +214,7 @@ ph_run_smoke_test(void) 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.strength != 6 || stats.attack != 6 || + stats.max_hp != 7 || stats.strength != 6 || 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"); @@ -219,6 +222,24 @@ ph_run_smoke_test(void) } world.entities[enemy_index].pos = (PhVec2){ player->pos.x - 5.0f, player->pos.y }; ph_world_tick(&world, (PhInput){ .move_x = -1 }, 1.0f); + ph_battle_begin(&battle, &world, enemy_index); + if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) != + PH_BATTLE_READY) { + fprintf(stderr, "smoke test failed: battle transition\n"); + return 1; + } + battle.command = PH_BATTLE_FIGHT; + ph_battle_order(&battle, &world, order); + if (order[0] != player_index || order[1] != enemy_index) { + fprintf(stderr, "smoke test failed: CTB order\n"); + return 1; + } + battle.command = PH_BATTLE_MAGICK; + ph_battle_select(&battle, &world); + if (battle.phase != PH_BATTLE_MAGIC || world.entities[player_index].mp != 5) { + fprintf(stderr, "smoke test failed: Magick submenu\n"); + return 1; + } if (world.encounter_index != enemy_index || ph_world_apply_accuracy_penalty(&world, enemy_index, 20, 3) < 0 || world.entities[enemy_index].accuracy_penalty != 20 || @@ -386,10 +407,10 @@ ph_draw_items(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *wor (size_t)def->asset_id < LEN(assets->textures)) texture = assets->textures[def->asset_id]; if (texture && def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0) { - rect.x = drop->pos.x - world->camera.pos.x - 8.0f; - rect.y = drop->pos.y - world->camera.pos.y - 8.0f; - rect.w = 16.0f; - rect.h = 16.0f; + rect.x = drop->pos.x - world->camera.pos.x - PH_TILE_SIZE * 0.5f; + rect.y = drop->pos.y - world->camera.pos.y - PH_TILE_SIZE * 0.5f; + rect.w = PH_TILE_SIZE; + rect.h = PH_TILE_SIZE; ph_draw_sprite(renderer, texture, def->sprite_tile_x, def->sprite_tile_y, rect, SDL_FLIP_NONE); } else { @@ -423,10 +444,10 @@ ph_draw_entities(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld * ph_entity_sprite_frame(def, entity, &sprite_tile_x, &sprite_tile_y, &flip); if (texture && sprite_tile_x >= 0 && sprite_tile_y >= 0) { - rect.x = entity->pos.x - world->camera.pos.x - 8.0f; - rect.y = entity->pos.y - world->camera.pos.y - 12.0f; - rect.w = 16.0f; - rect.h = 16.0f; + rect.x = entity->pos.x - world->camera.pos.x - PH_TILE_SIZE * 0.5f; + rect.y = entity->pos.y - world->camera.pos.y - PH_TILE_SIZE * 0.75f; + rect.w = PH_TILE_SIZE; + rect.h = PH_TILE_SIZE; ph_draw_sprite(renderer, texture, sprite_tile_x, sprite_tile_y, rect, flip); } else { @@ -464,11 +485,11 @@ ph_format_bonuses(char *text, size_t size, PhStats bonuses) text[0] = '\0'; ph_append_bonus(text, size, &used, bonuses.max_hp, "HP"); ph_append_bonus(text, size, &used, bonuses.max_mp, "MP"); - ph_append_bonus(text, size, &used, bonuses.strength + bonuses.attack, "STR"); + ph_append_bonus(text, size, &used, bonuses.strength, "STR"); ph_append_bonus(text, size, &used, bonuses.defense, "DEF"); ph_append_bonus(text, size, &used, bonuses.magic, "MAG"); ph_append_bonus(text, size, &used, bonuses.magic_defense, "MDEF"); - ph_append_bonus(text, size, &used, bonuses.agility + bonuses.speed, "AGI"); + ph_append_bonus(text, size, &used, bonuses.agility, "AGI"); ph_append_bonus(text, size, &used, bonuses.luck, "LCK"); ph_append_bonus(text, size, &used, bonuses.evasion, "EVA"); ph_append_bonus(text, size, &used, bonuses.accuracy, "ACC"); @@ -488,7 +509,7 @@ ph_draw_menu(SDL_Renderer *renderer, const char *title, SDL_Scancode key) SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); SDL_RenderDebugText(renderer, 24.0f, 22.0f, title); snprintf(text, sizeof(text), "CLOSE: %s", SDL_GetScancodeName(key)); - x = (float)(PH_VIEW_W - 20) - (float)strlen(text) * 8.0f; + x = ph_ui_right(20.0f) - (float)strlen(text) * PH_UI_FONT_W; if (x < 24.0f) x = 24.0f; SDL_RenderDebugText(renderer, x, (float)(PH_VIEW_H - 22), text); } @@ -505,7 +526,8 @@ ph_draw_character_sheet(SDL_Renderer *renderer, const PhAssets *assets, const PhEntity *player = ph_world_player(world); const PhEntityDef *def; PhStats stats; - SDL_FRect avatar = { 32.0f, 46.0f, 48.0f, 48.0f }; + SDL_FRect avatar = { 32.0f, 46.0f, PH_TILE_SIZE * 3.0f, + PH_TILE_SIZE * 3.0f }; char text[128]; int slot; @@ -528,17 +550,17 @@ ph_draw_character_sheet(SDL_Renderer *renderer, const PhAssets *assets, snprintf(text, sizeof(text), "DEF %d", stats.defense); SDL_RenderDebugText(renderer, 104.0f, 90.0f, text); snprintf(text, sizeof(text), "MAG %d", stats.magic); - SDL_RenderDebugText(renderer, 208.0f, 54.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 54.0f, text); snprintf(text, sizeof(text), "MDEF %d", stats.magic_defense); - SDL_RenderDebugText(renderer, 208.0f, 66.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 66.0f, text); snprintf(text, sizeof(text), "AGI %d", stats.agility); - SDL_RenderDebugText(renderer, 208.0f, 78.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 78.0f, text); snprintf(text, sizeof(text), "LCK %d", stats.luck); - SDL_RenderDebugText(renderer, 208.0f, 90.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 90.0f, text); snprintf(text, sizeof(text), "EVA %d", stats.evasion); - SDL_RenderDebugText(renderer, 208.0f, 102.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 102.0f, text); snprintf(text, sizeof(text), "ACC %d", stats.accuracy); - SDL_RenderDebugText(renderer, 208.0f, 114.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 114.0f, text); SDL_RenderDebugText(renderer, 24.0f, 128.0f, "EQUIPPED"); for (slot = PH_EQUIP_WEAPON; slot < PH_EQUIP_COUNT; ++slot) { @@ -551,7 +573,7 @@ ph_draw_character_sheet(SDL_Renderer *renderer, const PhAssets *assets, if (!item) continue; 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 = { 24.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE }; ph_draw_sprite(renderer, assets->textures[item->asset_id], item->sprite_tile_x, item->sprite_tile_y, icon, SDL_FLIP_NONE); } @@ -680,10 +702,10 @@ ph_draw_inventory_popup(SDL_Renderer *renderer, const PhWorld *world, 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 }; + (SDL_FRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } : + (SDL_FRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 124.0f }; char text[128]; - float x = ui->mode == PH_INVENTORY_EXAMINE ? 52.0f : 168.0f; + float x = ui->mode == PH_INVENTORY_EXAMINE ? 52.0f : ph_ui_right(152.0f); int i; SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR); @@ -706,7 +728,7 @@ ph_draw_inventory_popup(SDL_Renderer *renderer, const PhWorld *world, return; } - SDL_RenderDebugText(renderer, 168.0f, 58.0f, "ITEM ACTION"); + SDL_RenderDebugText(renderer, ph_ui_right(152.0f), 58.0f, "ITEM ACTION"); ph_inventory_action_order(item, actions); for (i = 0; i < PH_INVENTORY_ACTION_COUNT; ++i) { int action = actions[i]; @@ -717,7 +739,8 @@ ph_draw_inventory_popup(SDL_Renderer *renderer, const PhWorld *world, 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); + SDL_RenderDebugText(renderer, ph_ui_right(152.0f), + 78.0f + (float)i * 18.0f, text); } } @@ -753,7 +776,7 @@ ph_draw_inventory(SDL_Renderer *renderer, const PhAssets *assets, 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 = { 32.0f, y - 4.0f, 16.0f, 16.0f }; + SDL_FRect icon = { 32.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE }; ph_draw_sprite(renderer, assets->textures[item->asset_id], item->sprite_tile_x, item->sprite_tile_y, icon, SDL_FLIP_NONE); } @@ -771,12 +794,13 @@ ph_draw_inventory(SDL_Renderer *renderer, const PhAssets *assets, 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); + SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(56.0f), text); } - if (ui->status[0]) SDL_RenderDebugText(renderer, 24.0f, 200.0f, ui->status); + if (ui->status[0]) + SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status); snprintf(text, sizeof(text), "%s SELECT", SDL_GetScancodeName(PH_KEY_MENU_ACCEPT)); - SDL_RenderDebugText(renderer, 24.0f, 216.0f, text); + SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(24.0f), text); if (ui->selected >= 0 && ui->mode != PH_INVENTORY_BROWSE) ph_draw_inventory_popup(renderer, world, ui); } @@ -814,257 +838,6 @@ ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key) } } -static int -ph_battle_actor_delay(const PhWorld *world, int actor) -{ - PhStats stats = ph_world_entity_stats(world, &world->entities[actor]); - int delay = PH_BATTLE_CTB_BASE / (stats.agility > 0 ? stats.agility : 1); - - return delay > 0 ? delay : 1; -} - -static void -ph_battle_start_turn(PhBattle *battle, const PhWorld *world, int actor) -{ - battle->current_actor = actor; - battle->action_delay = ph_battle_actor_delay(world, actor); - battle->resolved = 0; - if (actor == world->player_index) { - battle->phase = PH_BATTLE_COMMAND; - battle->timer = 0.0f; - } else { - battle->phase = PH_BATTLE_ENEMY_ACTION; - battle->timer = PH_BATTLE_ACTION_SECONDS; - } -} - -static void -ph_battle_next_turn(PhBattle *battle, const PhWorld *world) -{ - int previous = battle->current_actor; - int next; - - if (previous == world->player_index) battle->player_ctb += battle->action_delay; - else battle->enemy_ctb += battle->action_delay; - if (battle->player_ctb < battle->enemy_ctb) next = world->player_index; - else if (battle->enemy_ctb < battle->player_ctb) next = battle->enemy_index; - else next = previous == world->player_index ? battle->enemy_index : world->player_index; - ph_battle_start_turn(battle, world, next); -} - -static void -ph_battle_begin(PhBattle *battle, const PhWorld *world, int enemy_index) -{ - const PhEntity *enemy = &world->entities[enemy_index]; - const PhEntityDef *def = ph_world_entity_def(world, enemy->type_id); - - memset(battle, 0, sizeof(*battle)); - battle->enemy_index = enemy_index; - battle->current_actor = world->player_index; - battle->enemy_ctb = ph_battle_actor_delay(world, enemy_index); - battle->timer = PH_BATTLE_TRANSITION_SECONDS; - if (def) { - battle->xp_reward = def->xp_reward; - battle->loot_item_id = def->loot_item_id; - battle->loot_amount = def->loot_amount; - snprintf(battle->message, sizeof(battle->message), "%s approaches!", def->name); - } -} - -static int -ph_battle_item(const PhWorld *world) -{ - const PhEntity *player = ph_world_player(world); - PhStats stats = ph_world_entity_stats(world, player); - int i; - - for (i = 0; i < world->item_def_count; ++i) - if (world->inventory[i] > 0 && world->item_defs[i].use != PH_ITEM_USE_NONE && - player && player->hp < stats.max_hp) return world->item_defs[i].id; - return 0; -} - -static const PhSpellDef * -ph_spell_def(int id) -{ - size_t i; - - for (i = 0; i < LEN(ph_spell_defs); ++i) - if (ph_spell_defs[i].id == id) return &ph_spell_defs[i]; - return NULL; -} - -static const PhSpellDef * -ph_battle_spell(const PhBattle *battle) -{ - if (battle->spell < 0 || (size_t)battle->spell >= LEN(ph_player_spells)) - return NULL; - return ph_spell_def(ph_player_spells[battle->spell]); -} - -static void -ph_battle_select(PhBattle *battle, PhWorld *world) -{ - if (battle->phase != PH_BATTLE_COMMAND || !ph_world_player(world) || - battle->current_actor != world->player_index) return; - if (battle->command == 1) { - battle->spell = 0; - battle->phase = PH_BATTLE_MAGIC; - snprintf(battle->message, sizeof(battle->message), "Choose a spell."); - return; - } - if (battle->command == 2 && !(battle->item_id = ph_battle_item(world))) { - snprintf(battle->message, sizeof(battle->message), "No usable item."); - return; - } - battle->phase = PH_BATTLE_PLAYER_ACTION; - battle->timer = PH_BATTLE_ACTION_SECONDS; - battle->resolved = 0; - battle->action_delay = ph_battle_actor_delay(world, world->player_index); - if (battle->action_delay < 1) battle->action_delay = 1; -} - -static void -ph_battle_cast(PhBattle *battle, PhWorld *world) -{ - const PhEntity *player = ph_world_player(world); - const PhSpellDef *spell = ph_battle_spell(battle); - - if (battle->phase != PH_BATTLE_MAGIC || !player || !spell) return; - if (player->mp < spell->mp_cost) { - snprintf(battle->message, sizeof(battle->message), "Not enough MP."); - return; - } - battle->phase = PH_BATTLE_PLAYER_ACTION; - battle->timer = PH_BATTLE_ACTION_SECONDS; - battle->resolved = 0; - battle->action_delay = ph_battle_actor_delay(world, world->player_index) * - spell->delay_percent / 100; - if (battle->action_delay < 1) battle->action_delay = 1; -} - -static void -ph_battle_order(const PhBattle *battle, const PhWorld *world, - int order[PH_BATTLE_ORDER_COUNT]) -{ - int player_ctb = battle->player_ctb; - int enemy_ctb = battle->enemy_ctb; - int actor = battle->current_actor; - int i; - - for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) { - int delay = ph_battle_actor_delay(world, actor); - const PhSpellDef *spell = ph_battle_spell(battle); - - order[i] = actor; - 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 == 1))) - delay = delay * spell->delay_percent / 100; - if (delay < 1) delay = 1; - } - if (actor == world->player_index) player_ctb += delay; - else enemy_ctb += delay; - if (player_ctb < enemy_ctb) actor = world->player_index; - else if (enemy_ctb < player_ctb) actor = battle->enemy_index; - else actor = actor == world->player_index ? - battle->enemy_index : world->player_index; - } -} - -static void -ph_battle_resolve(PhBattle *battle, PhWorld *world) -{ - const PhEntityDef *def; - const PhSpellDef *spell = NULL; - int damage; - - if (battle->phase == PH_BATTLE_PLAYER_ACTION) { - def = ph_world_entity_def(world, world->entities[battle->enemy_index].type_id); - if (battle->command == 0) { - damage = ph_world_physical_attack(world, world->player_index, - battle->enemy_index); - } else if (battle->command == 1) { - spell = ph_battle_spell(battle); - if (!spell) return; - damage = ph_world_magic_attack(world, world->player_index, - battle->enemy_index, spell->mp_cost, spell->power); - if (damage >= 0 && spell->accuracy_penalty > 0 && - spell->accuracy_turns > 0) - ph_world_apply_accuracy_penalty(world, battle->enemy_index, - spell->accuracy_penalty, spell->accuracy_turns); - } else { - const PhItemDef *item = ph_world_item_def(world, battle->item_id); - - damage = ph_world_use_inventory_item(world, world->player_index, - battle->item_id) < 0 ? -1 : 0; - snprintf(battle->message, sizeof(battle->message), "Used %s.", - item && item->name ? item->name : "item"); - return; - } - if (battle->command == 1 && damage >= 0) - snprintf(battle->message, sizeof(battle->message), - "%s deals %d damage.", spell->name, damage); - else - snprintf(battle->message, sizeof(battle->message), damage > 0 ? - "%s takes %d damage." : "The attack misses.", - def && def->name ? def->name : "Enemy", damage); - return; - } - - def = ph_world_entity_def(world, world->entities[battle->enemy_index].type_id); - damage = ph_world_physical_attack(world, battle->enemy_index, world->player_index); - ph_world_advance_effects(world, battle->enemy_index); - snprintf(battle->message, sizeof(battle->message), damage > 0 ? - "%s deals %d damage." : "%s misses.", - def && def->name ? def->name : "Enemy", damage); -} - -static void -ph_battle_update(PhBattle *battle, PhWorld *world, PhGameMode *game, float dt) -{ - if (*game == PH_GAME_TRANSITION) { - battle->timer -= dt; - if (battle->timer <= 0.0f) { - battle->timer = 0.0f; - ph_battle_start_turn(battle, world, world->player_index); - *game = PH_GAME_BATTLE; - } - return; - } - if (*game != PH_GAME_BATTLE || battle->phase == PH_BATTLE_COMMAND || - battle->phase == PH_BATTLE_MAGIC) return; - battle->timer -= dt; - if (!battle->resolved && battle->timer <= PH_BATTLE_ACTION_SECONDS * 0.5f) { - ph_battle_resolve(battle, world); - battle->resolved = 1; - } - if (battle->timer > 0.0f) return; - if (battle->phase == PH_BATTLE_PLAYER_ACTION) { - if (world->entities[battle->enemy_index].hp <= 0) { - if (ph_world_finish_encounter(world, battle->enemy_index) == 0) - *game = PH_GAME_VICTORY; - return; - } - } else if (world->entities[world->player_index].hp <= 0) { - *game = PH_GAME_OVER; - return; - } - ph_battle_next_turn(battle, world); -} - -static float -ph_battle_lunge(const PhBattle *battle, PhBattlePhase phase) -{ - float progress; - - if (battle->phase != phase || PH_BATTLE_ACTION_SECONDS <= 0.0f) return 0.0f; - progress = 1.0f - battle->timer / PH_BATTLE_ACTION_SECONDS; - return (progress < 0.5f ? progress : 1.0f - progress) * 20.0f; -} - static void ph_draw_battle_entity(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *world, int entity_index, SDL_FRect rect) @@ -1100,7 +873,11 @@ static void ph_draw_battle(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *world, const PhBattle *battle) { - static const char *commands[] = { "FIGHT", "MAGICK", "ITEM" }; + static const char *commands[PH_BATTLE_COMMAND_COUNT] = { + [PH_BATTLE_FIGHT] = "FIGHT", + [PH_BATTLE_MAGICK] = "MAGICK", + [PH_BATTLE_ITEM] = "ITEM", + }; 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); @@ -1108,14 +885,17 @@ ph_draw_battle(SDL_Renderer *renderer, const PhAssets *assets, 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, 104.0f, PH_VIEW_W, 72.0f }; - SDL_FRect panel = { 8.0f, 176.0f, PH_VIEW_W - 16.0f, 56.0f }; - SDL_FRect spell_panel = { 96.0f, 124.0f, 144.0f, + 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, 24.0f + (float)LEN(ph_player_spells) * 12.0f }; SDL_FRect enemy_rect = { 56.0f + ph_battle_lunge(battle, - PH_BATTLE_ENEMY_ACTION), 68.0f, 48.0f, 48.0f }; - SDL_FRect player_rect = { 232.0f - ph_battle_lunge(battle, - PH_BATTLE_PLAYER_ACTION), 120.0f, 48.0f, 48.0f }; + PH_BATTLE_ENEMY_ACTION), PH_VIEW_H - 172.0f, + PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f }; + SDL_FRect player_rect = { PH_VIEW_W - 88.0f - ph_battle_lunge(battle, + PH_BATTLE_PLAYER_ACTION), PH_VIEW_H - 120.0f, + PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f }; char text[96]; int i; @@ -1136,11 +916,12 @@ ph_draw_battle(SDL_Renderer *renderer, const PhAssets *assets, SDL_RenderDebugText(renderer, 16.0f, 42.0f, text); } ph_battle_order(battle, world, order); - SDL_RenderDebugText(renderer, 224.0f, 18.0f, "TURN ORDER"); + SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 18.0f, "TURN ORDER"); for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) { snprintf(text, sizeof(text), "%d %s", i + 1, order[i] == world->player_index ? "YOU" : "ENEMY"); - SDL_RenderDebugText(renderer, 240.0f, 30.0f + (float)i * 11.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(80.0f), + 30.0f + (float)i * 11.0f, text); } SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR); SDL_RenderFillRect(renderer, &panel); @@ -1150,35 +931,38 @@ ph_draw_battle(SDL_Renderer *renderer, const PhAssets *assets, SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); snprintf(text, sizeof(text), "%c %s", (battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC) && - battle->command == i ? '>' : ' ', + battle->command == (PhBattleCommand)i ? '>' : ' ', commands[i]); - SDL_RenderDebugText(renderer, 18.0f, 184.0f + (float)i * 14.0f, text); + SDL_RenderDebugText(renderer, 18.0f, + panel.y + 8.0f + (float)i * 14.0f, text); } SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); - SDL_RenderDebugText(renderer, 104.0f, 184.0f, battle->message); + SDL_RenderDebugText(renderer, 104.0f, panel.y + 8.0f, battle->message); if ((battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC) && - battle->command == 1 && spell) { + battle->command == PH_BATTLE_MAGICK && spell) { snprintf(text, sizeof(text), "%s %dMP QUICK", spell->name, spell->mp_cost); - SDL_RenderDebugText(renderer, 104.0f, 216.0f, text); + 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, 240.0f, 204.0f, text); + 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); - SDL_RenderDebugText(renderer, 240.0f, 216.0f, text); + SDL_RenderDebugText(renderer, ph_ui_right(80.0f), ph_ui_bottom(24.0f), text); if (battle->phase == PH_BATTLE_MAGIC) { SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR); SDL_RenderFillRect(renderer, &spell_panel); SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR); SDL_RenderRect(renderer, &spell_panel); SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); - SDL_RenderDebugText(renderer, 104.0f, 130.0f, "MAGICK"); + SDL_RenderDebugText(renderer, spell_panel.x + 8.0f, + spell_panel.y + 6.0f, "MAGICK"); for (i = 0; i < (int)LEN(ph_player_spells); ++i) { 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); - SDL_RenderDebugText(renderer, 104.0f, 144.0f + (float)i * 12.0f, text); + SDL_RenderDebugText(renderer, spell_panel.x + 8.0f, + spell_panel.y + 20.0f + (float)i * 12.0f, text); } } } @@ -1197,18 +981,23 @@ ph_draw_battle_end(SDL_Renderer *renderer, const PhWorld *world, SDL_RenderClear(renderer); SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR); if (game == PH_GAME_OVER) { - SDL_RenderDebugText(renderer, 120.0f, 108.0f, "GAME OVER"); + SDL_RenderDebugText(renderer, ph_ui_center_text("GAME OVER"), + PH_VIEW_H * 0.5f - 12.0f, "GAME OVER"); return; } - SDL_RenderDebugText(renderer, 128.0f, 76.0f, "VICTORY!"); + SDL_RenderDebugText(renderer, ph_ui_center_text("VICTORY!"), + PH_VIEW_H * 0.5f - 44.0f, "VICTORY!"); snprintf(text, sizeof(text), "XP GAINED %d", battle->xp_reward); - SDL_RenderDebugText(renderer, 96.0f, 106.0f, text); + SDL_RenderDebugText(renderer, ph_ui_center_text(text), + PH_VIEW_H * 0.5f - 14.0f, text); if (loot && battle->loot_amount > 0) snprintf(text, sizeof(text), "LOOT %s x%d", loot->name, battle->loot_amount); else snprintf(text, sizeof(text), "LOOT NONE"); - SDL_RenderDebugText(renderer, 72.0f, 124.0f, text); - SDL_RenderDebugText(renderer, 104.0f, 164.0f, "ENTER TO CONTINUE"); + SDL_RenderDebugText(renderer, ph_ui_center_text(text), + PH_VIEW_H * 0.5f + 4.0f, text); + SDL_RenderDebugText(renderer, ph_ui_center_text("ENTER TO CONTINUE"), + PH_VIEW_H * 0.5f + 44.0f, "ENTER TO CONTINUE"); } static void @@ -1216,6 +1005,8 @@ ph_render_frame(SDL_Renderer *renderer, const PhAssets *assets, PhWorld *world, PhInput input, float dt, PhMenu menu, const PhInventoryUi *inventory_ui, PhGameMode *game, PhBattle *battle) { + PhBattleResult battle_result = PH_BATTLE_CONTINUE; + if (*game == PH_GAME_WORLD && menu == PH_MENU_NONE) { ph_world_tick(world, input, dt); if (world->encounter_index >= 0) { @@ -1223,7 +1014,12 @@ ph_render_frame(SDL_Renderer *renderer, const PhAssets *assets, PhWorld *world, *game = PH_GAME_TRANSITION; } } - ph_battle_update(battle, world, game, dt); + if (*game == PH_GAME_TRANSITION || *game == PH_GAME_BATTLE) + battle_result = ph_battle_update(battle, world, dt, + *game == PH_GAME_TRANSITION); + if (battle_result == PH_BATTLE_READY) *game = PH_GAME_BATTLE; + else if (battle_result == PH_BATTLE_VICTORY) *game = PH_GAME_VICTORY; + else if (battle_result == PH_BATTLE_DEFEAT) *game = PH_GAME_OVER; if (*game == PH_GAME_BATTLE) { ph_draw_battle(renderer, assets, world, battle); SDL_RenderPresent(renderer); @@ -1300,7 +1096,7 @@ ph_create_window_renderer(SDL_Window **window, SDL_Renderer **renderer) *window = SDL_CreateWindow(PH_WINDOW_TITLE, PH_VIEW_W * PH_SCALE, PH_VIEW_H * PH_SCALE, - 0); + SDL_WINDOW_RESIZABLE); if (!*window) { fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); return -1; @@ -1430,17 +1226,19 @@ ph_run_render_smoke_test(void) world.entities[world.player_index].hp = ph_world_entity_stats(&world, ph_world_player(&world)).max_hp; ph_battle_begin(&battle, &world, enemy_index); - game = PH_GAME_BATTLE; - ph_battle_start_turn(&battle, &world, world.player_index); + game = PH_GAME_TRANSITION; + ph_render_frame(renderer, &assets, &world, (PhInput){ 0 }, + PH_BATTLE_TRANSITION_SECONDS, PH_MENU_NONE, &inventory_ui, + &game, &battle); world.entities[enemy_index].hp = ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp; - battle.command = 0; + battle.command = PH_BATTLE_FIGHT; ph_battle_order(&battle, &world, order); if (order[0] != world.player_index || order[1] != enemy_index) { fprintf(stderr, "render smoke test failed: normal CTB order\n"); result = 1; } - battle.command = 1; + 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"); @@ -1468,7 +1266,7 @@ ph_run_render_smoke_test(void) fprintf(stderr, "render smoke test failed: Flash action\n"); result = 1; } - battle.command = 0; + battle.command = PH_BATTLE_FIGHT; ph_battle_select(&battle, &world); ph_render_frame(renderer, &assets, &world, (PhInput){ 0 }, PH_BATTLE_ACTION_SECONDS * 0.5f, PH_MENU_NONE, &inventory_ui, @@ -1569,7 +1367,8 @@ ph_run_game(int frame_limit) else if (key == PH_KEY_MENU_CANCEL) battle.phase = PH_BATTLE_COMMAND; } else if (previous || next) { - battle.command = (battle.command + (previous ? 2 : 1)) % 3; + battle.command = (PhBattleCommand)((battle.command + (previous ? + PH_BATTLE_COMMAND_COUNT - 1 : 1)) % PH_BATTLE_COMMAND_COUNT); } else if (key == PH_KEY_MENU_ACCEPT) { ph_battle_select(&battle, &world); } diff --git a/src/tools/mapc.c b/src/tools/mapc.c @@ -1,4 +1,5 @@ #include "engine/world.h" +#include "game/content.h" #include "config.h" #include <stdint.h>