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:
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>