commit a0b75c0bdd0ebf17601ece20f251505282de3d50
parent cfc0364d7fcc8a45c91d65674d328fe2fee9d5e0
Author: beep <beep@wimdupont.com>
Date: Thu, 30 Jul 2026 08:29:56 +0000
Simplify engine state and world setup
Diffstat:
18 files changed, 526 insertions(+), 552 deletions(-)
diff --git a/Makefile b/Makefile
@@ -20,7 +20,6 @@ SRC = \
src/engine/battle.c \
src/engine/render.c \
src/engine/world.c \
- src/game/area.c \
src/game/content.c \
src/game/presentation.c \
src/game/main.c
diff --git a/README.adoc b/README.adoc
@@ -4,105 +4,61 @@
== Direction
-* FFVI-like top-down movement/camera feel.
-* No random encounters; use visible/in-world enemy encounters instead.
-* CTB combat uses agility and action speed to expose and manipulate turn order.
-* Loot-heavy character progression with talent points and build choices.
-* 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.
-
-Phantasia is a game, not a general-purpose engine product. "Engine-like" means
-that its systems operate on generic concepts such as entities, items,
-interactions, and map markers instead of named content such as Mira or
-Stonehollow. Named IDs belong in the content catalog and content-specific tests;
-initialization, world logic, and presentation treat those IDs as opaque data.
-
-The source tree enforces that boundary. `src/engine` contains reusable mechanics
-and must never include `game/*`. `src/game` owns Phantasia's content catalogs,
-starting-world composition, interface overlays, and application startup; it may
-depend on the engine. Code belongs in the engine whenever it operates entirely
-on generic engine types and caller-supplied data.
-
-Core world, area, and CTB battle logic are independent of SDL. `src/engine/area.c`
-loads caller-supplied area catalogs, instantiates generic markers, and handles
-portal travel while preserving inactive-area entity and item state.
-`src/engine/battle.c` owns the battle state machine and receives the game's spell
-book as data. The optional SDL module `src/engine/render.c` owns generic asset,
-sprite, tile, item, and entity rendering. `src/game/area.c` only composes the
-starting world from Phantasia's catalogs, while `src/game/presentation.c` owns
-game-specific menus and overlays. `src/game/main.c` composes those systems with
-input and application lifecycle.
-
-Headless engine coverage lives in `src/tests/smoke.c`; SDL rendering and
-presentation coverage lives in `src/tests/render.c` and builds as a separate
-executable. Production `main.c` contains no test fixtures.
-
-World actors update even while the player stands still. Entity content defines
-relative movement speed, directional animation frames, vision, and aggression;
-map-marker content defines each actor's home and wandering radius. Aggressive
-actors with clear sight pursue the player, remember the last visible position
-briefly, then return to their territory. Movement and sight both respect blocked
-map tiles, while reserved destinations keep moving entities from crossing
-through one another.
+* FFVI-like top-down movement and camera feel.
+* Visible enemies instead of random encounters.
+* FFX-inspired CTB combat whose turn order responds to agility and action speed.
+* Loot-heavy progression with meaningful build choices.
+* Optional, discoverable story that does not interrupt play.
+* A small set of clear rules that gains depth through interaction, like chess.
== Principles
-Phantasia's gameplay and implementation may change substantially while it is
-being explored. These principles should not:
-
-* Performance and simplicity come first, in both the game and its code.
-* Concentrate on the core gameplay loop and the core code before broadening the
- game. Put foundations in place that can be expanded through data and
- configuration instead of accumulating special cases.
-* The game should be easy to grasp. Like chess, a small set of clear rules can
- create depth through their interactions rather than through a large number of
- mechanics.
-* Prefer the smallest straightforward implementation that does the job. Remove,
- reuse, or combine code before adding another abstraction or special case.
-* Lines of code and file count are design costs. A feature that adds a
- substantial amount of code, or requires another file, needs proportionate
- value and careful consideration.
-* Consider every change carefully and verify its effects. Preserve existing
- behavior deliberately; avoid regression bugs and unintended side effects.
-* Values that are expected to vary belong in configuration, not fixed in core
- code. Follow the suckless `config.def.h`/`config.h` model: ship useful defaults
- while keeping local configuration compile-time, visible, and easy to change.
-* Small code must remain clear and correct. Fewer lines are a means to simplicity,
- not a reason to hide behavior or make the code cryptic.
-
-== Configuration
-
-The first build copies `config.def.h` to `config.h`. Edit `config.h` to change
-local compile-time settings; it is ignored by Git and is not overwritten by
-normal later builds. `config.def.h` contains the distributed defaults. The
-active `config.h` is the only source of configured values; core code does not
-silently supply missing gameplay, presentation, or control defaults.
-
-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 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, map-marker meanings, and
-rewards—lives in the typed `src/game/content.c`/`content.h` module. Maps own
-placement: their symbols place players, monsters, NPCs, items, portals, and
-arrival points. Content definitions map each symbol to a generic marker kind and
-its underlying terrain tile. Adding an instance should normally change only a
-map; adding a new content definition should change the content catalog and map
-without adding renderer or initialization special cases. Marker definitions also
-own instance-specific values such as territory radius or item amount; global
-world movement pace, actor decision timing, wandering chance, and pursuit memory
-remain settings in `config.h`.
-
-Items define distinct buy and sell prices. The player carries gold, and shop
-content defines stock and dialogue without embedding merchant-specific rules in
-the renderer. Potions restore HP; Ether restores MP.
-
-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.
+Phantasia's details may change while these principles remain stable:
+
+* Put performance, simplicity, and the core gameplay loop first.
+* Prefer the smallest clear implementation. Remove, reuse, or combine before
+ adding abstractions, special cases, files, or substantial code.
+* Build foundations that expand through typed data and configuration. New
+ content should not require initialization or rendering special cases.
+* Treat file count and lines of code as costs, but never trade clarity or
+ correctness for a smaller number.
+* Verify changes carefully and preserve existing behavior deliberately.
+* Keep compile-time settings visible through the suckless
+ `config.def.h`/`config.h` model.
+
+== Architecture
+
+Phantasia is a game, not a general-purpose engine product. Its systems are
+nevertheless engine-like: they operate on entities, items, interactions, area
+markers, and caller-supplied catalogs rather than named game content. Here,
+"engine" means the reusable internal layer, not a separately packaged framework
+or a promise of API compatibility.
+
+`src/engine` contains reusable systems and must never include `game/*`.
+`src/game` owns Phantasia's content, menus and overlays, and application startup;
+it may depend on the engine. Generic code belongs in the engine whenever it can
+operate entirely on engine types and supplied data.
+
+Core logic remains headless-testable. SDL rendering is an optional engine layer,
+while game-specific presentation and application orchestration stay outside the
+simulation.
+
+For the current implementation, start with `src/engine/*.h` for the engine's
+public data types and operations, `src/game/content.c` for a concrete game setup,
+and `src/tests/` for headless and SDL integration examples. Source remains
+authoritative for exact systems, types, and content as they evolve.
+
+== Content and configuration
+
+The first build copies tracked defaults from `config.def.h` to ignored local
+`config.h`. Later builds preserve `config.h`, which is the sole source of active
+settings. A configuration API version rejects stale files so missing values are
+never replaced by silent core-code fallbacks.
+
+Configuration owns adjustable settings; `game/content` owns typed game data.
+Maps own placement, while content assigns semantics to their markers. Adding an
+instance should normally change only a map; adding content should change the
+catalog and map rather than engine code.
== Build
@@ -111,59 +67,30 @@ desktop window size.
make
----
-If SDL3 is not installed, this still builds a headless logic binary that can run a smoke test:
+Without SDL3, build and run the headless smoke suite:
[source,sh]
----
make smoke
----
-To exercise the SDL renderer in a headless environment:
+Exercise SDL rendering in a headless environment:
[source,sh]
----
make render-smoke
----
-With SDL3 available, run the game normally:
+With SDL3 available:
[source,sh]
----
./bin/phantasia
-----
-
-Read the man page for controls:
-
-[source,sh]
-----
man ./phantasia.6
----
== Maps
-Map source files live in `data/maps/*.txt`.
-
-`make` compiles them into binary `obj/maps/*.phmap` files with `bin/ph-mapc`,
-and the game loads those binary map files at runtime.
-
-Current tile legend across the meadow and Stonehollow town maps:
-
-* `.` = walkable ground
-* `#` = blocked wall
-* `,` = walkable town path
-* `[`, `^`, `]`, `{`, `}`, `(`, `_`, `)` = blocked building parts
-* `=` and `+` = walkable building floor or doorway
-* `T` = visible portal icon produced from a portal marker
-
-Current content-marker legend:
-
-* `@` = player spawn
-* `a` = slime spawn
-* `m` = Mira spawn
-* `1` and `2` = item drops
-* `>` and `<` = area portals
-* `e` = portal arrival point
-
-Marker meanings are area-specific entries in `content.c`, not engine rules. The
-map compiler records their positions and replaces them with the configured
-underlying terrain, so marker symbols never become visible tiles at runtime.
+Sources live in `data/maps/*.txt`. `make` compiles them with `bin/ph-mapc` into
+`obj/maps/*.phmap`. Tile and marker meanings are typed data in `game/content.c`,
+which is the authoritative reference as maps evolve.
diff --git a/src/engine/area.c b/src/engine/area.c
@@ -35,6 +35,37 @@ ph_area_catalog_load(PhArea *area, const PhAreaCatalog *catalog, int area_id)
return 0;
}
+int
+ph_world_start(PhWorld *world, const PhWorldSetup *setup,
+ float viewport_w, float viewport_h)
+{
+ PhArea area;
+ int player = -1;
+ int i;
+
+ if (!setup || !setup->areas || setup->equipment_count < 0 ||
+ (setup->equipment_count && !setup->equipment) ||
+ ph_area_catalog_load(&area, setup->areas, setup->start_area_id) < 0)
+ return -1;
+ if (ph_world_init(world, area, setup->start_area_id, setup->content,
+ viewport_w, viewport_h) < 0) {
+ ph_area_free(&area);
+ return -1;
+ }
+ if (ph_world_prepare_area(world, &world->area, setup->areas,
+ setup->start_area_id, &player) < 0 || player < 0) goto fail;
+ for (i = 0; i < setup->equipment_count; ++i)
+ if (setup->equipment[i] &&
+ ph_world_equip_item(world, player, setup->equipment[i]) < 0)
+ goto fail;
+ ph_world_set_player(world, player);
+ return 0;
+
+fail:
+ ph_area_free(&world->area);
+ return -1;
+}
+
static PhVec2
ph_marker_pos(const PhAreaMarker *marker)
{
diff --git a/src/engine/area.h b/src/engine/area.h
@@ -30,17 +30,27 @@ typedef struct {
typedef struct {
const PhAreaDef *areas;
- int area_count;
const PhMarkerDef *markers;
- int marker_count;
const PhTileDef *tiles;
+ int area_count;
+ int marker_count;
int tile_count;
} PhAreaCatalog;
+typedef struct {
+ const PhAreaCatalog *areas;
+ const int *equipment;
+ PhWorldCatalog content;
+ int start_area_id;
+ int equipment_count;
+} PhWorldSetup;
+
const PhAreaDef *ph_area_catalog_area(const PhAreaCatalog *catalog, int area_id);
const PhMarkerDef *ph_area_catalog_marker(const PhAreaCatalog *catalog,
int area_id, unsigned char symbol);
int ph_area_catalog_load(PhArea *area, const PhAreaCatalog *catalog, int area_id);
+int ph_world_start(PhWorld *world, const PhWorldSetup *setup,
+ float viewport_w, float viewport_h);
int ph_world_prepare_area(PhWorld *world, const PhArea *area,
const PhAreaCatalog *catalog, int area_id, int *player);
int ph_world_update_portal(PhWorld *world, const PhAreaCatalog *catalog);
diff --git a/src/engine/battle.c b/src/engine/battle.c
@@ -19,11 +19,21 @@ ph_battle_actor_delay(const PhWorld *world, int actor)
return delay > 0 ? delay : 1;
}
+static int
+ph_battle_next_actor(const PhBattle *battle, const PhWorld *world,
+ int player_ctb, int enemy_ctb, int previous)
+{
+ if (player_ctb < enemy_ctb) return world->player_index;
+ if (enemy_ctb < player_ctb) return battle->enemy_index;
+ return previous == world->player_index ?
+ battle->enemy_index : world->player_index;
+}
+
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->turn.current_actor = actor;
+ battle->turn.action_delay = ph_battle_actor_delay(world, actor);
battle->resolved = 0;
if (actor == world->player_index) {
battle->phase = PH_BATTLE_COMMAND;
@@ -37,15 +47,13 @@ ph_battle_start_turn(PhBattle *battle, const PhWorld *world, int actor)
static void
ph_battle_next_turn(PhBattle *battle, const PhWorld *world)
{
- int previous = battle->current_actor;
- int next;
+ int previous = battle->turn.current_actor;
- 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);
+ if (previous == world->player_index)
+ battle->turn.player_ctb += battle->turn.action_delay;
+ else battle->turn.enemy_ctb += battle->turn.action_delay;
+ ph_battle_start_turn(battle, world, ph_battle_next_actor(battle, world,
+ battle->turn.player_ctb, battle->turn.enemy_ctb, previous));
}
static int
@@ -55,12 +63,12 @@ ph_battle_item(const PhWorld *world)
PhStats stats = ph_world_entity_stats(world, player);
int i;
- for (i = 0; i < world->item_def_count; ++i)
+ for (i = 0; i < world->content.item_count; ++i)
if (world->inventory[i] > 0 && player &&
- ((world->item_defs[i].use == PH_ITEM_USE_HEAL &&
- player->hp < stats.max_hp) ||
- (world->item_defs[i].use == PH_ITEM_USE_RESTORE_MP &&
- player->mp < stats.max_mp))) return world->item_defs[i].id;
+ ((world->content.items[i].use == PH_ITEM_USE_HEAL &&
+ player->vitals.hp < stats.max_hp) ||
+ (world->content.items[i].use == PH_ITEM_USE_RESTORE_MP &&
+ player->vitals.mp < stats.max_mp))) return world->content.items[i].id;
return 0;
}
@@ -77,9 +85,9 @@ ph_spell_def(const PhBattle *battle, int id)
const PhSpellDef *
ph_battle_spell(const PhBattle *battle)
{
- if (battle->spell < 0 || battle->spell >= battle->spells.learned_count)
+ if (battle->choice.spell < 0 || battle->choice.spell >= battle->spells.learned_count)
return NULL;
- return ph_spell_def(battle, battle->spells.learned[battle->spell]);
+ return ph_spell_def(battle, battle->spells.learned[battle->choice.spell]);
}
void
@@ -92,13 +100,11 @@ ph_battle_begin(PhBattle *battle, const PhWorld *world, int enemy_index,
memset(battle, 0, sizeof(*battle));
battle->spells = spells;
battle->enemy_index = enemy_index;
- battle->current_actor = world->player_index;
- battle->enemy_ctb = ph_battle_actor_delay(world, enemy_index);
+ battle->turn.current_actor = world->player_index;
+ battle->turn.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;
+ battle->reward = def->reward;
snprintf(battle->message, sizeof(battle->message), "%s approaches!", def->name);
}
}
@@ -107,22 +113,22 @@ 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->turn.current_actor != world->player_index) return;
+ if (battle->choice.command == PH_BATTLE_MAGICK) {
+ battle->choice.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))) {
+ if (battle->choice.command == PH_BATTLE_ITEM &&
+ !(battle->choice.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);
+ battle->turn.action_delay = ph_battle_actor_delay(world, world->player_index);
}
void
@@ -132,16 +138,16 @@ ph_battle_cast(PhBattle *battle, PhWorld *world)
const PhSpellDef *spell = ph_battle_spell(battle);
if (battle->phase != PH_BATTLE_MAGIC || !player || !spell) return;
- if (player->mp < spell->mp_cost) {
+ if (player->vitals.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) *
+ battle->turn.action_delay = ph_battle_actor_delay(world, world->player_index) *
spell->delay_percent / 100;
- if (battle->action_delay < 1) battle->action_delay = 1;
+ if (battle->turn.action_delay < 1) battle->turn.action_delay = 1;
}
void
@@ -154,9 +160,9 @@ 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 player_ctb = battle->turn.player_ctb;
+ int enemy_ctb = battle->turn.enemy_ctb;
+ int actor = battle->turn.current_actor;
int i;
for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) {
@@ -166,17 +172,14 @@ ph_battle_order(const PhBattle *battle, const PhWorld *world,
order[i] = actor;
if (i == 0 && actor == world->player_index) {
if (battle->phase == PH_BATTLE_PLAYER_ACTION)
- delay = battle->action_delay;
+ delay = battle->turn.action_delay;
else if (spell && battle->phase == PH_BATTLE_MAGIC)
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;
+ actor = ph_battle_next_actor(battle, world, player_ctb, enemy_ctb, actor);
}
}
@@ -189,28 +192,28 @@ ph_battle_resolve(PhBattle *battle, PhWorld *world)
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) {
+ if (battle->choice.command == PH_BATTLE_FIGHT) {
damage = ph_world_physical_attack(world, world->player_index,
battle->enemy_index);
- } else if (battle->command == PH_BATTLE_MAGICK) {
+ } else if (battle->choice.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)
+ if (damage >= 0 && spell->accuracy.value > 0 &&
+ spell->accuracy.turns > 0)
ph_world_apply_accuracy_penalty(world, battle->enemy_index,
- spell->accuracy_penalty, spell->accuracy_turns);
+ spell->accuracy.value, spell->accuracy.turns);
} else {
- const PhItemDef *item = ph_world_item_def(world, battle->item_id);
+ const PhItemDef *item = ph_world_item_def(world, battle->choice.item_id);
damage = ph_world_use_inventory_item(world, world->player_index,
- battle->item_id) < 0 ? -1 : 0;
+ battle->choice.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)
+ if (battle->choice.command == PH_BATTLE_MAGICK && damage >= 0)
snprintf(battle->message, sizeof(battle->message),
"%s deals %d damage.", spell->name, damage);
else
@@ -248,13 +251,13 @@ ph_battle_update(PhBattle *battle, PhWorld *world, float dt, int transition)
}
if (battle->timer > 0.0f) return PH_BATTLE_CONTINUE;
if (battle->phase == PH_BATTLE_PLAYER_ACTION &&
- world->entities[battle->enemy_index].hp <= 0) {
+ world->entities[battle->enemy_index].vitals.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)
+ world->entities[world->player_index].vitals.hp <= 0)
return PH_BATTLE_DEFEAT;
ph_battle_next_turn(battle, world);
return PH_BATTLE_CONTINUE;
diff --git a/src/engine/battle.h b/src/engine/battle.h
@@ -32,30 +32,35 @@ typedef struct {
int mp_cost;
int power;
int delay_percent;
- int accuracy_penalty;
- int accuracy_turns;
+ PhTimedEffect accuracy;
} PhSpellDef;
typedef struct {
const PhSpellDef *defs;
- int def_count;
const int *learned;
+ int def_count;
int learned_count;
} PhSpellBook;
typedef struct {
- int enemy_index;
PhBattleCommand command;
int spell;
int item_id;
- int resolved;
- int xp_reward;
- int loot_item_id;
- int loot_amount;
+} PhBattleChoice;
+
+typedef struct {
int player_ctb;
int enemy_ctb;
int current_actor;
int action_delay;
+} PhBattleTurn;
+
+typedef struct {
+ int enemy_index;
+ PhBattleChoice choice;
+ PhBattleTurn turn;
+ int resolved;
+ PhReward reward;
float timer;
PhBattlePhase phase;
char message[64];
diff --git a/src/engine/render.c b/src/engine/render.c
@@ -14,19 +14,20 @@ ph_world_pixel(float position, float camera)
}
static SDL_Texture *
-ph_load_sprite_sheet(SDL_Renderer *renderer, const char *path,
- Uint8 key_r, Uint8 key_g, Uint8 key_b)
+ph_load_sprite_sheet(SDL_Renderer *renderer, const PhAssetDef *def)
{
SDL_Surface *surface;
SDL_Texture *texture;
- surface = SDL_LoadBMP(path);
+ surface = SDL_LoadBMP(def->path);
if (!surface) {
- fprintf(stderr, "SDL_LoadBMP failed for %s: %s\n", path, SDL_GetError());
+ fprintf(stderr, "SDL_LoadBMP failed for %s: %s\n",
+ def->path, SDL_GetError());
return NULL;
}
- SDL_SetSurfaceColorKey(surface, true, SDL_MapSurfaceRGB(surface, key_r, key_g, key_b));
+ SDL_SetSurfaceColorKey(surface, true, SDL_MapSurfaceRGB(surface,
+ def->key.r, def->key.g, def->key.b));
texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_DestroySurface(surface);
if (!texture) {
@@ -47,8 +48,7 @@ ph_render_assets_load(PhRenderAssets *assets, SDL_Renderer *renderer,
assets->texture_count = def_count;
for (i = 0; i < def_count; ++i) {
if (!defs[i].path) continue;
- assets->textures[i] = ph_load_sprite_sheet(renderer, defs[i].path,
- defs[i].key_r, defs[i].key_g, defs[i].key_b);
+ assets->textures[i] = ph_load_sprite_sheet(renderer, &defs[i]);
if (!assets->textures[i]) {
ph_render_assets_destroy(assets);
return -1;
@@ -92,13 +92,13 @@ void
ph_render_entity_frame(const PhEntityDef *def, const PhEntity *entity,
int *sprite_tile_x, int *sprite_tile_y, SDL_FlipMode *flip)
{
- int direction = entity->facing_y > 0 ? PH_SPRITE_DOWN :
- entity->facing_y < 0 ? PH_SPRITE_UP : PH_SPRITE_SIDE;
+ int direction = entity->motion.facing_y > 0 ? PH_SPRITE_DOWN :
+ entity->motion.facing_y < 0 ? PH_SPRITE_UP : PH_SPRITE_SIDE;
int frame = ph_entity_animation_frame(def, entity);
*sprite_tile_x = def->sprite_tiles[direction][frame][0];
*sprite_tile_y = def->sprite_tiles[direction][frame][1];
- *flip = direction == PH_SPRITE_SIDE && entity->facing_x > 0 ?
+ *flip = direction == PH_SPRITE_SIDE && entity->motion.facing_x > 0 ?
SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
}
diff --git a/src/engine/render.h b/src/engine/render.h
@@ -6,10 +6,14 @@
enum { PH_MAX_ASSETS = 32 };
typedef struct {
+ unsigned char r;
+ unsigned char g;
+ unsigned char b;
+} PhColorKey;
+
+typedef struct {
const char *path;
- unsigned char key_r;
- unsigned char key_g;
- unsigned char key_b;
+ PhColorKey key;
} PhAssetDef;
#if PH_USE_SDL
diff --git a/src/engine/world.c b/src/engine/world.c
@@ -38,12 +38,8 @@ typedef struct {
static float
ph_clampf(float value, float min, float max)
{
- if (value < min) {
- return min;
- }
- if (value > max) {
- return max;
- }
+ if (value < min) return min;
+ if (value > max) return max;
return value;
}
@@ -75,11 +71,8 @@ ph_entity_def_index(const PhWorld *world, int type_id)
{
int i;
- for (i = 0; i < world->entity_def_count; ++i) {
- if (world->entity_defs[i].id == type_id) {
- return i;
- }
- }
+ for (i = 0; i < world->content.entity_count; ++i)
+ if (world->content.entities[i].id == type_id) return i;
return -1;
}
@@ -88,11 +81,8 @@ ph_item_def_index(const PhWorld *world, int item_id)
{
int i;
- for (i = 0; i < world->item_def_count; ++i) {
- if (world->item_defs[i].id == item_id) {
- return i;
- }
- }
+ for (i = 0; i < world->content.item_count; ++i)
+ if (world->content.items[i].id == item_id) return i;
return -1;
}
@@ -104,18 +94,12 @@ ph_camera_follow_player(PhWorld *world)
float max_y;
player = ph_world_player(world);
- if (!player) {
- return;
- }
+ if (!player) return;
max_x = (float)(world->area.width * PH_TILE_SIZE) - world->camera.viewport_w;
max_y = (float)(world->area.height * PH_TILE_SIZE) - world->camera.viewport_h;
- if (max_x < 0.0f) {
- max_x = 0.0f;
- }
- if (max_y < 0.0f) {
- max_y = 0.0f;
- }
+ if (max_x < 0.0f) max_x = 0.0f;
+ if (max_y < 0.0f) max_y = 0.0f;
world->camera.pos.x = ph_clampf(player->pos.x - world->camera.viewport_w * 0.5f, 0.0f, max_x);
world->camera.pos.y = ph_clampf(player->pos.y - world->camera.viewport_h * 0.5f, 0.0f, max_y);
@@ -133,8 +117,8 @@ ph_position_blocked(const PhWorld *world, PhVec2 pos)
static void
ph_world_set_notice(PhWorld *world, const char *text)
{
- snprintf(world->notice, sizeof(world->notice), "%s", text);
- world->notice_seconds = PH_NOTICE_SECONDS;
+ snprintf(world->notice.text, sizeof(world->notice.text), "%s", text);
+ world->notice.seconds = PH_NOTICE_SECONDS;
}
static const PhEntity *
@@ -159,7 +143,7 @@ ph_world_blocking_entity(const PhWorld *world, int self_index, PhVec2 pos)
(float)(PH_ENTITY_BLOCK_RADIUS * PH_ENTITY_BLOCK_RADIUS)) {
return entity;
}
- if (entity->moving && ph_dist2(entity->move_target, pos) <
+ if (entity->motion.moving && ph_dist2(entity->motion.target, pos) <
(float)(PH_ENTITY_BLOCK_RADIUS * PH_ENTITY_BLOCK_RADIUS)) {
return entity;
}
@@ -238,8 +222,8 @@ static void
ph_try_interact(PhWorld *world, const PhEntity *player)
{
PhVec2 target = {
- player->pos.x + (float)(player->facing_x * PH_TILE_SIZE),
- player->pos.y + (float)(player->facing_y * PH_TILE_SIZE),
+ player->pos.x + (float)(player->motion.facing_x * PH_TILE_SIZE),
+ player->pos.y + (float)(player->motion.facing_y * PH_TILE_SIZE),
};
float closest = PH_INTERACT_RADIUS * PH_INTERACT_RADIUS;
int i;
@@ -317,16 +301,16 @@ ph_entity_begin_step(PhWorld *world, int entity_index, int dx, int dy,
entity->pos.x + (float)(dx * PH_TILE_SIZE),
entity->pos.y + (float)(dy * PH_TILE_SIZE),
};
- float radius = (float)(entity->territory_radius * PH_TILE_SIZE);
+ float radius = (float)(entity->behavior.territory_radius * PH_TILE_SIZE);
- if ((!dx && !dy) || entity->moving) return 0;
- entity->facing_x = dx;
- entity->facing_y = dy;
- if (stay_in_territory && entity->territory_radius > 0 &&
- ph_dist2(target, entity->home) > radius * radius) return 0;
+ if ((!dx && !dy) || entity->motion.moving) return 0;
+ entity->motion.facing_x = dx;
+ entity->motion.facing_y = dy;
+ if (stay_in_territory && entity->behavior.territory_radius > 0 &&
+ ph_dist2(target, entity->behavior.home) > radius * radius) return 0;
if (ph_entity_step_blocked(world, entity_index, target)) return 0;
- entity->move_target = target;
- entity->moving = 1;
+ entity->motion.target = target;
+ entity->motion.moving = 1;
return 1;
}
@@ -354,24 +338,24 @@ ph_world_actor_action(PhWorld *world, int entity_index)
const PhEntityDef *def = ph_world_entity_def(world, actor->type_id);
int direction;
- if (!def || def->kind == PH_ENTITY_PLAYER || !actor->active || actor->hp <= 0)
+ if (!def || def->kind == PH_ENTITY_PLAYER || !actor->active || actor->vitals.hp <= 0)
return;
if (ph_actor_sees_player(world, actor, def)) {
- actor->last_seen = ph_world_player(world)->pos;
- actor->chase_turns = PH_MOB_MEMORY_TURNS;
- ph_entity_step_toward(world, entity_index, actor->last_seen);
+ actor->behavior.last_seen = ph_world_player(world)->pos;
+ actor->behavior.chase_turns = PH_MOB_MEMORY_TURNS;
+ ph_entity_step_toward(world, entity_index, actor->behavior.last_seen);
return;
}
- if (actor->chase_turns > 0) {
- --actor->chase_turns;
- ph_entity_step_toward(world, entity_index, actor->last_seen);
+ if (actor->behavior.chase_turns > 0) {
+ --actor->behavior.chase_turns;
+ ph_entity_step_toward(world, entity_index, actor->behavior.last_seen);
return;
}
- if (actor->territory_radius > 0) {
- float radius = (float)(actor->territory_radius * PH_TILE_SIZE);
+ if (actor->behavior.territory_radius > 0) {
+ float radius = (float)(actor->behavior.territory_radius * PH_TILE_SIZE);
- if (ph_dist2(actor->pos, actor->home) > radius * radius) {
- ph_entity_step_toward(world, entity_index, actor->home);
+ if (ph_dist2(actor->pos, actor->behavior.home) > radius * radius) {
+ ph_entity_step_toward(world, entity_index, actor->behavior.home);
return;
}
}
@@ -399,27 +383,27 @@ ph_world_advance_movers(PhWorld *world, float dt)
float animation_cycle;
int animation_frames;
- if (!entity->active || !entity->moving ||
+ if (!entity->active || !entity->motion.moving ||
entity->area_id != world->area_id) continue;
def = ph_world_entity_def(world, entity->type_id);
if (!def || def->move_speed <= 0) continue;
- dx = entity->move_target.x - entity->pos.x;
- dy = entity->move_target.y - entity->pos.y;
+ dx = entity->motion.target.x - entity->pos.x;
+ dy = entity->motion.target.y - entity->pos.y;
distance = sqrtf(dx * dx + dy * dy);
travel = (float)(PH_TILE_SIZE * def->move_speed) * dt / step_seconds;
moved = distance < travel ? distance : travel;
- entity->animation_distance += moved;
+ entity->motion.animation_distance += moved;
animation_frames = def->animation_frames > 0 &&
def->animation_frames <= PH_ANIMATION_FRAMES ?
def->animation_frames : 1;
animation_cycle = (float)(animation_frames *
PH_WORLD_ANIMATION_PIXELS_PER_FRAME);
- if (entity->animation_distance >= animation_cycle)
- entity->animation_distance = fmodf(entity->animation_distance,
+ if (entity->motion.animation_distance >= animation_cycle)
+ entity->motion.animation_distance = fmodf(entity->motion.animation_distance,
animation_cycle);
if (distance <= travel || distance <= 0.001f) {
- entity->pos = entity->move_target;
- entity->moving = 0;
+ entity->pos = entity->motion.target;
+ entity->motion.moving = 0;
} else {
entity->pos.x += dx / distance * travel;
entity->pos.y += dy / distance * travel;
@@ -439,26 +423,32 @@ ph_world_update_actors(PhWorld *world, float dt)
if (!def || def->kind == PH_ENTITY_PLAYER || def->move_speed <= 0 ||
!entity->active || entity->area_id != world->area_id) continue;
- entity->move_credit += (float)def->move_speed * dt;
- if (entity->move_credit > action_seconds) entity->move_credit = action_seconds;
- if (entity->moving || entity->move_credit < action_seconds) continue;
- entity->move_credit -= action_seconds;
+ entity->motion.credit += (float)def->move_speed * dt;
+ if (entity->motion.credit > action_seconds) entity->motion.credit = action_seconds;
+ if (entity->motion.moving || entity->motion.credit < action_seconds) continue;
+ entity->motion.credit -= action_seconds;
ph_world_actor_action(world, i);
}
}
-void
+int
ph_world_init(PhWorld *world, PhArea area, int area_id,
- float viewport_w, float viewport_h)
+ PhWorldCatalog content, float viewport_w, float viewport_h)
{
+ if (content.entity_count < 0 || content.entity_count > PH_MAX_ENTITY_TYPES ||
+ content.item_count < 0 || content.item_count > PH_MAX_ITEM_TYPES ||
+ (content.entity_count && !content.entities) ||
+ (content.item_count && !content.items)) return -1;
memset(world, 0, sizeof(*world));
world->area = area;
world->area_id = area_id;
+ world->content = content;
world->camera.viewport_w = viewport_w;
world->camera.viewport_h = viewport_h;
world->player_index = -1;
world->encounter_index = -1;
world->rng_state = 0x5048414eu;
+ return 0;
}
void
@@ -475,8 +465,8 @@ ph_world_enter_area(PhWorld *world, PhArea area, int area_id, PhVec2 player_pos)
&world->entities[world->player_index] : NULL;
if (!player) return;
player->area_id = area_id;
- player->pos = player->move_target = player->home = player_pos;
- player->moving = 0;
+ player->pos = player->motion.target = player->behavior.home = player_pos;
+ player->motion.moving = 0;
ph_camera_follow_player(world);
}
@@ -582,26 +572,6 @@ ph_area_free(PhArea *area)
}
int
-ph_world_add_entity_def(PhWorld *world, PhEntityDef def)
-{
- if (world->entity_def_count >= PH_MAX_ENTITY_TYPES) {
- return -1;
- }
- world->entity_defs[world->entity_def_count++] = def;
- return 0;
-}
-
-int
-ph_world_add_item_def(PhWorld *world, PhItemDef def)
-{
- if (world->item_def_count >= PH_MAX_ITEM_TYPES) {
- return -1;
- }
- world->item_defs[world->item_def_count++] = def;
- return 0;
-}
-
-int
ph_world_spawn_entity(PhWorld *world, int type_id, PhVec2 pos,
int territory_radius, int area_id)
{
@@ -622,15 +592,15 @@ ph_world_spawn_entity(PhWorld *world, int type_id, PhVec2 pos,
entity->type_id = type_id;
entity->area_id = area_id;
entity->pos = pos;
- entity->move_target = pos;
- entity->home = pos;
- entity->last_seen = pos;
- entity->hp = def->stats.max_hp;
- entity->mp = def->stats.max_mp;
+ entity->motion.target = pos;
+ entity->behavior.home = pos;
+ entity->behavior.last_seen = pos;
+ entity->vitals.hp = def->stats.max_hp;
+ entity->vitals.mp = def->stats.max_mp;
entity->gold = def->starting_gold;
- entity->facing_x = 0;
- entity->facing_y = 1;
- entity->territory_radius = territory_radius > 0 ? territory_radius : 0;
+ entity->motion.facing_x = 0;
+ entity->motion.facing_y = 1;
+ entity->behavior.territory_radius = territory_radius > 0 ? territory_radius : 0;
entity->active = 1;
return world->entity_count++;
@@ -692,12 +662,12 @@ ph_world_equip_item(PhWorld *world, int entity_index, int item_id)
before = ph_world_entity_stats(world, entity);
entity->equipment[slot] = item_id;
after = ph_world_entity_stats(world, entity);
- entity->hp += after.max_hp - before.max_hp;
- entity->mp += after.max_mp - before.max_mp;
- if (entity->hp < 0) entity->hp = 0;
- if (entity->hp > after.max_hp) entity->hp = after.max_hp;
- if (entity->mp < 0) entity->mp = 0;
- if (entity->mp > after.max_mp) entity->mp = after.max_mp;
+ entity->vitals.hp += after.max_hp - before.max_hp;
+ entity->vitals.mp += after.max_mp - before.max_mp;
+ if (entity->vitals.hp < 0) entity->vitals.hp = 0;
+ if (entity->vitals.hp > after.max_hp) entity->vitals.hp = after.max_hp;
+ if (entity->vitals.mp < 0) entity->vitals.mp = 0;
+ if (entity->vitals.mp > after.max_mp) entity->vitals.mp = after.max_mp;
return 0;
}
@@ -737,12 +707,12 @@ ph_world_use_inventory_item(PhWorld *world, int entity_index, int item_id)
entity = &world->entities[entity_index];
stats = ph_world_entity_stats(world, entity);
if (item->use_power <= 0) return -1;
- if (item->use == PH_ITEM_USE_HEAL && entity->hp < stats.max_hp) {
- entity->hp += item->use_power;
- if (entity->hp > stats.max_hp) entity->hp = stats.max_hp;
- } else if (item->use == PH_ITEM_USE_RESTORE_MP && entity->mp < stats.max_mp) {
- entity->mp += item->use_power;
- if (entity->mp > stats.max_mp) entity->mp = stats.max_mp;
+ if (item->use == PH_ITEM_USE_HEAL && entity->vitals.hp < stats.max_hp) {
+ entity->vitals.hp += item->use_power;
+ if (entity->vitals.hp > stats.max_hp) entity->vitals.hp = stats.max_hp;
+ } else if (item->use == PH_ITEM_USE_RESTORE_MP && entity->vitals.mp < stats.max_mp) {
+ entity->vitals.mp += item->use_power;
+ if (entity->vitals.mp > stats.max_mp) entity->vitals.mp = stats.max_mp;
} else {
return -1;
}
@@ -816,19 +786,19 @@ ph_world_physical_attack(PhWorld *world, int attacker_index, int target_index)
target_index < 0 || target_index >= world->entity_count) return -1;
attacker = &world->entities[attacker_index];
target = &world->entities[target_index];
- if (!attacker->active || !target->active || attacker->hp <= 0 || target->hp <= 0)
+ if (!attacker->active || !target->active || attacker->vitals.hp <= 0 || target->vitals.hp <= 0)
return -1;
attack = ph_world_entity_stats(world, attacker);
defense = ph_world_entity_stats(world, target);
accuracy = attack.accuracy * (100 -
- ph_clampi(attacker->accuracy_penalty, 0, 100)) / 100;
+ ph_clampi(attacker->accuracy.value, 0, 100)) / 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 / 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);
+ target->vitals.hp = ph_clampi(target->vitals.hp - damage, 0, defense.max_hp);
return damage;
}
@@ -842,8 +812,8 @@ ph_world_apply_accuracy_penalty(PhWorld *world, int entity_index,
percent <= 0 || turns <= 0) return -1;
entity = &world->entities[entity_index];
if (!entity->active) return -1;
- entity->accuracy_penalty = ph_clampi(percent, 0, 100);
- entity->accuracy_turns = turns;
+ entity->accuracy.value = ph_clampi(percent, 0, 100);
+ entity->accuracy.turns = turns;
return 0;
}
@@ -854,8 +824,8 @@ ph_world_advance_effects(PhWorld *world, int entity_index)
if (entity_index < 0 || entity_index >= world->entity_count) return;
entity = &world->entities[entity_index];
- if (entity->accuracy_turns > 0 && --entity->accuracy_turns == 0)
- entity->accuracy_penalty = 0;
+ if (entity->accuracy.turns > 0 && --entity->accuracy.turns == 0)
+ entity->accuracy.value = 0;
}
int
@@ -872,14 +842,14 @@ ph_world_magic_attack(PhWorld *world, int attacker_index, int target_index,
mp_cost < 0 || power < 0) return -1;
attacker = &world->entities[attacker_index];
target = &world->entities[target_index];
- if (!attacker->active || !target->active || attacker->hp <= 0 ||
- target->hp <= 0 || attacker->mp < mp_cost) return -1;
+ if (!attacker->active || !target->active || attacker->vitals.hp <= 0 ||
+ target->vitals.hp <= 0 || attacker->vitals.mp < mp_cost) return -1;
defense = ph_world_entity_stats(world, target);
damage = ph_world_entity_stats(world, attacker).magic + power -
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);
+ attacker->vitals.mp -= mp_cost;
+ target->vitals.hp = ph_clampi(target->vitals.hp - damage, 0, defense.max_hp);
return damage;
}
@@ -897,13 +867,13 @@ ph_world_finish_encounter(PhWorld *world, int enemy_index)
player = &world->entities[world->player_index];
enemy = &world->entities[enemy_index];
def = ph_world_entity_def(world, enemy->type_id);
- if (!def || def->kind != PH_ENTITY_MONSTER || enemy->hp > 0) return -1;
- if (def->loot_item_id && def->loot_amount > 0) {
- item_index = ph_item_def_index(world, def->loot_item_id);
+ if (!def || def->kind != PH_ENTITY_MONSTER || enemy->vitals.hp > 0) return -1;
+ if (def->reward.item_id && def->reward.amount > 0) {
+ item_index = ph_item_def_index(world, def->reward.item_id);
if (item_index < 0) return -1;
}
- player->xp += def->xp_reward;
- if (item_index >= 0) world->inventory[item_index] += def->loot_amount;
+ player->xp += def->reward.xp;
+ if (item_index >= 0) world->inventory[item_index] += def->reward.amount;
enemy->active = 0;
world->encounter_index = -1;
return 0;
@@ -925,22 +895,22 @@ ph_world_tick(PhWorld *world, PhInput input, float dt)
if (!player->active || !def) {
return;
}
- if (world->notice_seconds > 0.0f) {
- world->notice_seconds -= dt;
- if (world->notice_seconds <= 0.0f) {
- world->notice_seconds = 0.0f;
- world->notice[0] = '\0';
+ if (world->notice.seconds > 0.0f) {
+ world->notice.seconds -= dt;
+ if (world->notice.seconds <= 0.0f) {
+ world->notice.seconds = 0.0f;
+ world->notice.text[0] = '\0';
}
}
input = ph_orthogonal_input(input);
world->interaction_id = 0;
- was_moving = player->moving;
- if (!player->moving && def->move_speed > 0)
+ was_moving = player->motion.moving;
+ if (!player->motion.moving && def->move_speed > 0)
ph_entity_begin_step(world, world->player_index,
input.move_x, input.move_y, 0);
ph_world_advance_movers(world, dt);
- if (was_moving && !player->moving && world->encounter_index < 0 &&
+ if (was_moving && !player->motion.moving && world->encounter_index < 0 &&
def->move_speed > 0)
ph_entity_begin_step(world, world->player_index,
input.move_x, input.move_y, 0);
@@ -959,10 +929,10 @@ ph_entity_animation_frame(const PhEntityDef *def, const PhEntity *entity)
{
int frames;
- if (!def || !entity || !entity->moving) return 0;
+ if (!def || !entity || !entity->motion.moving) return 0;
frames = def->animation_frames > 0 &&
def->animation_frames <= PH_ANIMATION_FRAMES ? def->animation_frames : 1;
- return (int)(entity->animation_distance /
+ return (int)(entity->motion.animation_distance /
PH_WORLD_ANIMATION_PIXELS_PER_FRAME) % frames;
}
@@ -979,14 +949,14 @@ const PhEntityDef *
ph_world_entity_def(const PhWorld *world, int type_id)
{
int i = ph_entity_def_index(world, type_id);
- return i >= 0 ? &world->entity_defs[i] : NULL;
+ return i >= 0 ? &world->content.entities[i] : NULL;
}
const PhItemDef *
ph_world_item_def(const PhWorld *world, int item_id)
{
int i = ph_item_def_index(world, item_id);
- return i >= 0 ? &world->item_defs[i] : NULL;
+ return i >= 0 ? &world->content.items[i] : NULL;
}
int
diff --git a/src/engine/world.h b/src/engine/world.h
@@ -67,6 +67,43 @@ typedef struct {
} PhStats;
typedef struct {
+ int hp;
+ int mp;
+} PhVitals;
+
+typedef struct {
+ PhVec2 target;
+ float credit;
+ float animation_distance;
+ int facing_x;
+ int facing_y;
+ int moving;
+} PhEntityMotion;
+
+typedef struct {
+ PhVec2 home;
+ PhVec2 last_seen;
+ int territory_radius;
+ int chase_turns;
+} PhEntityBehavior;
+
+typedef struct {
+ int value;
+ int turns;
+} PhTimedEffect;
+
+typedef struct {
+ int xp;
+ int item_id;
+ int amount;
+} PhReward;
+
+typedef struct {
+ char text[PH_NOTICE_SIZE];
+ float seconds;
+} PhNotice;
+
+typedef struct {
unsigned char symbol;
int blocks_movement;
int asset_id;
@@ -83,9 +120,7 @@ typedef struct {
int aggressive;
int starting_gold;
int interaction_id;
- int xp_reward;
- int loot_item_id;
- int loot_amount;
+ PhReward reward;
int asset_id;
int sprite_tiles[PH_SPRITE_DIRECTIONS][PH_ANIMATION_FRAMES][2];
int animation_frames;
@@ -109,25 +144,22 @@ typedef struct {
} PhItemDef;
typedef struct {
+ const PhEntityDef *entities;
+ const PhItemDef *items;
+ int entity_count;
+ int item_count;
+} PhWorldCatalog;
+
+typedef struct {
int type_id;
int area_id;
PhVec2 pos;
- PhVec2 move_target;
- PhVec2 home;
- PhVec2 last_seen;
- int hp;
- int mp;
+ PhEntityMotion motion;
+ PhEntityBehavior behavior;
+ PhVitals vitals;
int xp;
int gold;
- int accuracy_penalty;
- int accuracy_turns;
- int facing_x;
- int facing_y;
- int territory_radius;
- int chase_turns;
- int moving;
- float move_credit;
- float animation_distance;
+ PhTimedEffect accuracy;
int equipment[PH_EQUIP_COUNT];
int active;
} PhEntity;
@@ -167,14 +199,11 @@ typedef struct {
typedef struct {
PhArea area;
PhCamera camera;
- PhEntityDef entity_defs[PH_MAX_ENTITY_TYPES];
- PhItemDef item_defs[PH_MAX_ITEM_TYPES];
+ PhWorldCatalog content;
PhEntity entities[PH_MAX_ENTITIES];
PhGroundItem ground_items[PH_MAX_GROUND_ITEMS];
int player_index;
int area_id;
- int entity_def_count;
- int item_def_count;
int entity_count;
int ground_item_count;
int inventory[PH_MAX_ITEM_TYPES];
@@ -183,8 +212,7 @@ typedef struct {
int encounter_index;
int interaction_id;
unsigned int rng_state;
- char notice[PH_NOTICE_SIZE];
- float notice_seconds;
+ PhNotice notice;
} PhWorld;
typedef struct {
@@ -196,12 +224,10 @@ typedef struct {
int ph_area_load(PhArea *area, const char *path);
void ph_area_free(PhArea *area);
-void ph_world_init(PhWorld *world, PhArea area, int area_id,
- float viewport_w, float viewport_h);
+int ph_world_init(PhWorld *world, PhArea area, int area_id,
+ PhWorldCatalog content, float viewport_w, float viewport_h);
void ph_world_enter_area(PhWorld *world, PhArea area, int area_id,
PhVec2 player_pos);
-int ph_world_add_entity_def(PhWorld *world, PhEntityDef def);
-int ph_world_add_item_def(PhWorld *world, PhItemDef def);
int ph_world_spawn_entity(PhWorld *world, int type_id, PhVec2 pos,
int territory_radius, int area_id);
int ph_world_drop_item(PhWorld *world, int item_id, PhVec2 pos, int amount,
diff --git a/src/game/area.c b/src/game/area.c
@@ -1,34 +0,0 @@
-#include "game/area.h"
-
-#include "engine/area.h"
-#include "game/content.h"
-
-#define LEN(a) (sizeof(a) / sizeof(0[a]))
-
-int
-ph_game_world_init(PhWorld *world, float viewport_w, float viewport_h)
-{
- PhArea area;
- int player = -1;
- size_t i;
-
- if (ph_area_catalog_load(&area, &ph_area_catalog, PH_START_AREA) < 0)
- return -1;
- ph_world_init(world, area, PH_START_AREA, viewport_w, viewport_h);
- for (i = 0; i < LEN(ph_entity_defs); ++i)
- if (ph_world_add_entity_def(world, ph_entity_defs[i]) < 0) goto fail;
- for (i = 0; i < LEN(ph_item_defs); ++i)
- if (ph_world_add_item_def(world, ph_item_defs[i]) < 0) goto fail;
- if (ph_world_prepare_area(world, &world->area, &ph_area_catalog,
- PH_START_AREA, &player) < 0 || player < 0) goto fail;
- for (i = 0; i < LEN(ph_player_equipment); ++i)
- if (ph_player_equipment[i] &&
- ph_world_equip_item(world, player, ph_player_equipment[i]) < 0)
- goto fail;
- ph_world_set_player(world, player);
- return 0;
-
-fail:
- ph_area_free(&world->area);
- return -1;
-}
diff --git a/src/game/area.h b/src/game/area.h
@@ -1,8 +0,0 @@
-#ifndef PH_GAME_AREA_H
-#define PH_GAME_AREA_H
-
-#include "engine/world.h"
-
-int ph_game_world_init(PhWorld *world, float viewport_w, float viewport_h);
-
-#endif
diff --git a/src/game/content.c b/src/game/content.c
@@ -1,10 +1,10 @@
#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 },
+ [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 PhAreaDef ph_area_defs[PH_AREA_COUNT] = {
@@ -30,13 +30,16 @@ const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = {
};
const PhSpellDef ph_spell_defs[PH_SPELL_DEF_COUNT] = {
- { PH_SPELL_FLASH, "Flash", 15, 5, 50, 20, 3 },
+ { PH_SPELL_FLASH, "Flash", 15, 5, 50, { 20, 3 } },
};
const int ph_player_spells[PH_PLAYER_SPELL_COUNT] = { PH_SPELL_FLASH };
const PhSpellBook ph_player_spellbook = {
- ph_spell_defs, PH_SPELL_DEF_COUNT, ph_player_spells, PH_PLAYER_SPELL_COUNT,
+ .defs = ph_spell_defs,
+ .learned = ph_player_spells,
+ .def_count = PH_SPELL_DEF_COUNT,
+ .learned_count = PH_PLAYER_SPELL_COUNT,
};
const PhEntityDef ph_entity_defs[PH_ENTITY_DEF_COUNT] = {
@@ -67,9 +70,7 @@ const PhEntityDef ph_entity_defs[PH_ENTITY_DEF_COUNT] = {
.move_speed = 1,
.vision = 6,
.aggressive = 1,
- .xp_reward = 8,
- .loot_item_id = PH_ITEM_HEALING_POTION,
- .loot_amount = 1,
+ .reward = { 8, PH_ITEM_HEALING_POTION, 1 },
.asset_id = PH_ASSET_CRITTERS,
.sprite_tiles = {
[PH_SPRITE_DOWN] = { { 12, 0 }, { 13, 0 }, { 14, 0 }, { 15, 0 } },
@@ -159,7 +160,7 @@ const PhItemDef ph_item_defs[PH_ITEM_DEF_COUNT] = {
},
};
-const int ph_player_equipment[PH_EQUIP_COUNT] = {
+static const int ph_player_equipment[PH_EQUIP_COUNT] = {
[PH_EQUIP_WEAPON] = PH_ITEM_WAYFARER_BLADE,
[PH_EQUIP_ARMOR] = PH_ITEM_TRAVELER_COAT,
};
@@ -182,10 +183,26 @@ const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT] = {
{ PH_AREA_STONEHOLLOW, 'e', ',', PH_MARKER_ARRIVAL, .content_id = 0 },
};
-const PhAreaCatalog ph_area_catalog = {
- ph_area_defs, PH_AREA_COUNT,
- ph_marker_defs, PH_MARKER_DEF_COUNT,
- ph_tile_defs, PH_TILE_DEF_COUNT,
+static const PhAreaCatalog ph_area_catalog = {
+ .areas = ph_area_defs,
+ .markers = ph_marker_defs,
+ .tiles = ph_tile_defs,
+ .area_count = PH_AREA_COUNT,
+ .marker_count = PH_MARKER_DEF_COUNT,
+ .tile_count = PH_TILE_DEF_COUNT,
+};
+
+const PhWorldSetup ph_game_world = {
+ .areas = &ph_area_catalog,
+ .equipment = ph_player_equipment,
+ .content = {
+ .entities = ph_entity_defs,
+ .items = ph_item_defs,
+ .entity_count = PH_ENTITY_DEF_COUNT,
+ .item_count = PH_ITEM_DEF_COUNT,
+ },
+ .start_area_id = PH_START_AREA,
+ .equipment_count = PH_EQUIP_COUNT,
};
const PhShopDef ph_shop_defs[PH_SHOP_COUNT] = {
diff --git a/src/game/content.h b/src/game/content.h
@@ -85,9 +85,8 @@ 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 PhSpellBook ph_player_spellbook;
-extern const int ph_player_equipment[PH_EQUIP_COUNT];
extern const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT];
-extern const PhAreaCatalog ph_area_catalog;
+extern const PhWorldSetup ph_game_world;
extern const PhShopDef ph_shop_defs[PH_SHOP_COUNT];
extern const PhInteractionDef ph_interaction_defs[PH_INTERACTION_COUNT];
diff --git a/src/game/main.c b/src/game/main.c
@@ -1,6 +1,5 @@
#include "engine/battle.h"
#include "engine/area.h"
-#include "game/area.h"
#include "game/content.h"
#include "game/presentation.h"
@@ -29,7 +28,7 @@ ph_update_game(PhGameContext *context, PhInput input, float dt)
if (*game == PH_GAME_WORLD && context->menu == PH_MENU_NONE) {
ph_world_tick(world, input, dt);
- if (ph_world_update_portal(world, &ph_area_catalog) < 0)
+ if (ph_world_update_portal(world, ph_game_world.areas) < 0)
fprintf(stderr, "failed to enter area\n");
if (world->encounter_index >= 0) {
ph_battle_begin(context->battle, world, world->encounter_index,
@@ -105,7 +104,8 @@ ph_run_game(void)
fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return 1;
}
- if (ph_game_world_init(&world, (float)PH_VIEW_W, (float)PH_VIEW_H) < 0) {
+ if (ph_world_start(&world, &ph_game_world,
+ (float)PH_VIEW_W, (float)PH_VIEW_H) < 0) {
SDL_Quit();
return 1;
}
@@ -146,7 +146,7 @@ ph_run_game(void)
if (battle.phase == PH_BATTLE_MAGIC) {
if ((previous || next) && battle.spells.learned_count > 0)
- battle.spell = (battle.spell + (previous ?
+ battle.choice.spell = (battle.choice.spell + (previous ?
battle.spells.learned_count - 1 : 1)) %
battle.spells.learned_count;
else if (key == PH_KEY_MENU_ACCEPT)
@@ -154,7 +154,7 @@ ph_run_game(void)
else if (key == PH_KEY_MENU_CANCEL)
ph_battle_cancel(&battle);
} else if (previous || next) {
- battle.command = (PhBattleCommand)((battle.command + (previous ?
+ battle.choice.command = (PhBattleCommand)((battle.choice.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/game/presentation.c b/src/game/presentation.c
@@ -139,9 +139,9 @@ ph_draw_character_sheet(SDL_Renderer *renderer, const PhRenderAssets *assets,
def->sprite_tiles[PH_SPRITE_DOWN][0][1],
avatar, SDL_FLIP_NONE);
- snprintf(text, sizeof(text), "HP %d / %d", player->hp, stats.max_hp);
+ snprintf(text, sizeof(text), "HP %d / %d", player->vitals.hp, stats.max_hp);
SDL_RenderDebugText(renderer, 104.0f, 54.0f, text);
- snprintf(text, sizeof(text), "MP %d / %d", player->mp, stats.max_mp);
+ snprintf(text, sizeof(text), "MP %d / %d", player->vitals.mp, stats.max_mp);
SDL_RenderDebugText(renderer, 104.0f, 66.0f, text);
snprintf(text, sizeof(text), "STR %d", stats.strength);
SDL_RenderDebugText(renderer, 104.0f, 78.0f, text);
@@ -188,10 +188,10 @@ ph_inventory_step(const PhWorld *world, int selected, int direction)
int i = selected < 0 ? (direction > 0 ? -1 : 0) : selected;
int step;
- for (step = 0; step < world->item_def_count; ++step) {
+ for (step = 0; step < world->content.item_count; ++step) {
i += direction;
- if (i < 0) i = world->item_def_count - 1;
- if (i >= world->item_def_count) i = 0;
+ if (i < 0) i = world->content.item_count - 1;
+ if (i >= world->content.item_count) i = 0;
if (world->inventory[i] > 0) return i;
}
return -1;
@@ -219,7 +219,7 @@ ph_inventory_action(PhInventoryUi *ui, PhWorld *world, int action)
int result = -1;
if (ui->selected < 0) return;
- item = &world->item_defs[ui->selected];
+ item = &world->content.items[ui->selected];
name = item->name ? item->name : "Item";
if (action == PH_INVENTORY_EQUIP)
result = ph_world_equip_inventory_item(world, world->player_index, item->id);
@@ -298,7 +298,7 @@ ph_draw_inventory_popup(SDL_Renderer *renderer, const PhWorld *world,
const PhInventoryUi *ui)
{
static const char *action_names[] = { "EQUIP", "USE", "DROP", "EXAMINE" };
- const PhItemDef *item = &world->item_defs[ui->selected];
+ const PhItemDef *item = &world->content.items[ui->selected];
int actions[PH_INVENTORY_ACTION_COUNT];
SDL_FRect popup = ui->mode == PH_INVENTORY_EXAMINE ?
(SDL_FRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } :
@@ -356,15 +356,15 @@ ph_draw_inventory(SDL_Renderer *renderer, const PhRenderAssets *assets,
int page;
int i;
- for (i = 0; i < world->item_def_count; ++i)
+ for (i = 0; i < world->content.item_count; ++i)
if (world->inventory[i] > 0) {
if (i == ui->selected) selected_rank = total;
++total;
}
page = selected_rank / 5;
ph_draw_menu(renderer, "INVENTORY", PH_KEY_INVENTORY);
- for (i = 0; i < world->item_def_count; ++i) {
- const PhItemDef *item = &world->item_defs[i];
+ for (i = 0; i < world->content.item_count; ++i) {
+ const PhItemDef *item = &world->content.items[i];
SDL_Texture *texture;
float y;
@@ -431,7 +431,7 @@ ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key)
if (ui->mode == PH_INVENTORY_BROWSE && ui->selected >= 0) {
int actions[PH_INVENTORY_ACTION_COUNT];
- ph_inventory_action_order(&world->item_defs[ui->selected], actions);
+ ph_inventory_action_order(&world->content.items[ui->selected], actions);
ui->mode = PH_INVENTORY_ACTIONS;
ui->option = actions[0];
ui->status[0] = '\0';
@@ -444,7 +444,7 @@ ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key)
}
if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return 0;
if (ui->mode == PH_INVENTORY_ACTIONS) {
- ui->option = ph_inventory_action_step(&world->item_defs[ui->selected],
+ ui->option = ph_inventory_action_step(&world->content.items[ui->selected],
ui->option, previous ? -1 : 1);
} else {
ui->selected = ph_inventory_step(world, ui->selected, previous ? -1 : 1);
@@ -497,7 +497,7 @@ ph_shop_key(PhShopUi *ui, PhWorld *world, SDL_Scancode key)
}
if (previous || next) {
int count = ui->mode == PH_SHOP_ROOT ? 3 :
- ui->mode == PH_SHOP_BUY ? shop->item_count : world->item_def_count;
+ ui->mode == PH_SHOP_BUY ? shop->item_count : world->content.item_count;
if (ui->mode == PH_SHOP_SELL) {
ui->selected = ph_inventory_step(world, ui->selected,
@@ -523,7 +523,7 @@ ph_shop_key(PhShopUi *ui, PhWorld *world, SDL_Scancode key)
else
snprintf(ui->status, sizeof(ui->status), "Bought %s.", item->name);
} else if (ui->mode == PH_SHOP_SELL && ui->selected >= 0) {
- const PhItemDef *item = &world->item_defs[ui->selected];
+ const PhItemDef *item = &world->content.items[ui->selected];
if (ph_world_sell_item(world, world->player_index, item->id) < 0)
snprintf(ui->status, sizeof(ui->status), "Cannot sell that.");
@@ -570,8 +570,8 @@ ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world, const PhShopUi *ui)
int row = 0;
SDL_RenderDebugText(renderer, 24.0f, 42.0f, "SELL");
- for (i = 0; i < world->item_def_count; ++i) {
- const PhItemDef *item = &world->item_defs[i];
+ for (i = 0; i < world->content.item_count; ++i) {
+ const PhItemDef *item = &world->content.items[i];
if (world->inventory[i] <= 0) continue;
snprintf(text, sizeof(text), "%c %-16s x%d %d G",
@@ -663,11 +663,11 @@ ph_draw_battle(SDL_Renderer *renderer, const PhRenderAssets *assets,
SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
SDL_RenderDebugText(renderer, 16.0f, 18.0f,
enemy_def && enemy_def->name ? enemy_def->name : "ENEMY");
- snprintf(text, sizeof(text), "HP %d/%d", enemy->hp, enemy_stats.max_hp);
+ snprintf(text, sizeof(text), "HP %d/%d", enemy->vitals.hp, enemy_stats.max_hp);
SDL_RenderDebugText(renderer, 16.0f, 30.0f, text);
- if (enemy->accuracy_turns > 0) {
- snprintf(text, sizeof(text), "ACC -%d%% (%d)", enemy->accuracy_penalty,
- enemy->accuracy_turns);
+ if (enemy->accuracy.turns > 0) {
+ snprintf(text, sizeof(text), "ACC -%d%% (%d)", enemy->accuracy.value,
+ enemy->accuracy.turns);
SDL_RenderDebugText(renderer, 16.0f, 42.0f, text);
}
ph_battle_order(battle, world, order);
@@ -686,16 +686,16 @@ ph_draw_battle(SDL_Renderer *renderer, const PhRenderAssets *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 == (PhBattleCommand)i ? '>' : ' ',
+ battle->choice.command == (PhBattleCommand)i ? '>' : ' ',
commands[i]);
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, panel.y + 8.0f, battle->message);
- snprintf(text, sizeof(text), "HP %d/%d", player->hp, player_stats.max_hp);
+ snprintf(text, sizeof(text), "HP %d/%d", player->vitals.hp, player_stats.max_hp);
SDL_RenderDebugText(renderer, ph_ui_right(80.0f), ph_ui_bottom(36.0f), text);
- snprintf(text, sizeof(text), "MP %d/%d", player->mp, player_stats.max_mp);
+ snprintf(text, sizeof(text), "MP %d/%d", player->vitals.mp, player_stats.max_mp);
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);
@@ -711,7 +711,7 @@ ph_draw_battle(SDL_Renderer *renderer, const PhRenderAssets *assets,
if (!choice) continue;
snprintf(text, sizeof(text), "%c %s %dMP%s",
- battle->spell == i ? '>' : ' ', choice->name, choice->mp_cost,
+ battle->choice.spell == i ? '>' : ' ', choice->name, choice->mp_cost,
choice->delay_percent < 100 ? " QUICK" : "");
SDL_RenderDebugText(renderer, spell_panel.x + 8.0f,
spell_panel.y + 20.0f + (float)i * 12.0f, text);
@@ -723,7 +723,7 @@ static void
ph_draw_battle_end(SDL_Renderer *renderer, const PhWorld *world,
const PhBattle *battle, PhGameMode game)
{
- const PhItemDef *loot = ph_world_item_def(world, battle->loot_item_id);
+ const PhItemDef *loot = ph_world_item_def(world, battle->reward.item_id);
char text[96];
if (game == PH_GAME_VICTORY)
@@ -739,11 +739,12 @@ ph_draw_battle_end(SDL_Renderer *renderer, const PhWorld *world,
}
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);
+ snprintf(text, sizeof(text), "XP GAINED %d", battle->reward.xp);
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);
+ if (loot && battle->reward.amount > 0)
+ snprintf(text, sizeof(text), "LOOT %s x%d", loot->name,
+ battle->reward.amount);
else
snprintf(text, sizeof(text), "LOOT NONE");
SDL_RenderDebugText(renderer, ph_ui_center_text(text),
@@ -780,7 +781,7 @@ ph_present_frame(SDL_Renderer *renderer, const PhRenderAssets *assets,
ph_draw_inventory(renderer, assets, world, context->inventory);
} else if (context->menu == PH_MENU_SHOP) {
ph_draw_shop(renderer, world, context->shop);
- } else if (world->notice_seconds > 0.0f && world->notice[0] != '\0') {
+ } else if (world->notice.seconds > 0.0f && world->notice.text[0] != '\0') {
SDL_FRect notice_bg = {
.x = 8.0f,
.y = (float)(PH_VIEW_H - 28),
@@ -791,7 +792,8 @@ ph_present_frame(SDL_Renderer *renderer, const PhRenderAssets *assets,
SDL_SetRenderDrawColor(renderer, PH_NOTICE_COLOR);
SDL_RenderFillRect(renderer, ¬ice_bg);
SDL_SetRenderDrawColor(renderer, PH_NOTICE_TEXT_COLOR);
- SDL_RenderDebugText(renderer, 14.0f, (float)(PH_VIEW_H - 22), world->notice);
+ SDL_RenderDebugText(renderer, 14.0f, (float)(PH_VIEW_H - 22),
+ world->notice.text);
}
if (game == PH_GAME_TRANSITION) {
float progress = 1.0f - battle->timer / PH_BATTLE_TRANSITION_SECONDS;
diff --git a/src/tests/render.c b/src/tests/render.c
@@ -1,6 +1,5 @@
#include "engine/battle.h"
#include "engine/area.h"
-#include "game/area.h"
#include "game/content.h"
#include "game/presentation.h"
@@ -50,7 +49,8 @@ main(void)
return 1;
}
- if (ph_game_world_init(&world, (float)PH_VIEW_W, (float)PH_VIEW_H) < 0) {
+ if (ph_world_start(&world, &ph_game_world,
+ (float)PH_VIEW_W, (float)PH_VIEW_H) < 0) {
SDL_DestroyRenderer(renderer);
SDL_DestroySurface(surface);
SDL_Quit();
@@ -153,13 +153,13 @@ main(void)
battle.phase = PH_BATTLE_ENEMY_ACTION;
battle.timer = 0.0f;
battle.resolved = 1;
- world.entities[world.player_index].hp = 0;
+ world.entities[world.player_index].vitals.hp = 0;
context.menu = PH_MENU_NONE;
battle_result = ph_battle_update(&battle, &world, 0.0f, 0);
if (battle_result == PH_BATTLE_DEFEAT) game = PH_GAME_OVER;
ph_present_frame(renderer, &assets, &context);
if (game != PH_GAME_OVER) result = 1;
- world.entities[world.player_index].hp =
+ world.entities[world.player_index].vitals.hp =
ph_world_entity_stats(&world, ph_world_player(&world)).max_hp;
ph_battle_begin(&battle, &world, enemy_index, ph_player_spellbook);
game = PH_GAME_TRANSITION;
@@ -167,22 +167,22 @@ main(void)
if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) ==
PH_BATTLE_READY) game = PH_GAME_BATTLE;
ph_present_frame(renderer, &assets, &context);
- world.entities[enemy_index].hp =
+ world.entities[enemy_index].vitals.hp =
ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp;
- battle.command = PH_BATTLE_FIGHT;
+ battle.choice.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 = PH_BATTLE_MAGICK;
+ battle.choice.command = PH_BATTLE_MAGICK;
ph_battle_order(&battle, &world, order);
if (order[0] != world.player_index || order[1] != enemy_index) {
fprintf(stderr, "render smoke test failed: command CTB order\n");
result = 1;
}
ph_battle_select(&battle, &world);
- if (battle.phase != PH_BATTLE_MAGIC || ph_world_player(&world)->mp != 50) {
+ if (battle.phase != PH_BATTLE_MAGIC || ph_world_player(&world)->vitals.mp != 50) {
fprintf(stderr, "render smoke test failed: Magick submenu\n");
result = 1;
}
@@ -201,14 +201,14 @@ main(void)
ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
ph_present_frame(renderer, &assets, &context);
if (game != PH_GAME_BATTLE || battle.phase != PH_BATTLE_COMMAND ||
- battle.current_actor != world.player_index ||
- ph_world_player(&world)->mp != 35 ||
- world.entities[enemy_index].accuracy_penalty != 20 ||
- world.entities[enemy_index].accuracy_turns != 3) {
+ battle.turn.current_actor != world.player_index ||
+ ph_world_player(&world)->vitals.mp != 35 ||
+ world.entities[enemy_index].accuracy.value != 20 ||
+ world.entities[enemy_index].accuracy.turns != 3) {
fprintf(stderr, "render smoke test failed: Flash action\n");
result = 1;
}
- battle.command = PH_BATTLE_FIGHT;
+ battle.choice.command = PH_BATTLE_FIGHT;
ph_battle_select(&battle, &world);
context.menu = PH_MENU_NONE;
ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
@@ -218,15 +218,16 @@ main(void)
PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
if (battle_result == PH_BATTLE_VICTORY) game = PH_GAME_VICTORY;
ph_present_frame(renderer, &assets, &context);
- if (game != PH_GAME_VICTORY || ph_world_player(&world)->xp != battle.xp_reward ||
- ph_world_item_amount(&world, battle.loot_item_id) != 2) {
+ if (game != PH_GAME_VICTORY ||
+ ph_world_player(&world)->xp != battle.reward.xp ||
+ ph_world_item_amount(&world, battle.reward.item_id) != 2) {
fprintf(stderr, "render smoke test failed: victory rewards\n");
result = 1;
}
}
game = PH_GAME_WORLD;
for (i = 0; i < world.area.marker_count; ++i) {
- const PhMarkerDef *def = ph_area_catalog_marker(&ph_area_catalog,
+ const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas,
world.area_id, world.area.markers[i].symbol);
if (!def || def->kind != PH_MARKER_PORTAL) continue;
@@ -236,7 +237,7 @@ main(void)
break;
}
if (i == world.area.marker_count ||
- ph_world_update_portal(&world, &ph_area_catalog) != 1) {
+ ph_world_update_portal(&world, ph_game_world.areas) != 1) {
fprintf(stderr, "render smoke test failed: town portal\n");
result = 1;
}
diff --git a/src/tests/smoke.c b/src/tests/smoke.c
@@ -1,7 +1,6 @@
#include "engine/area.h"
#include "engine/world.h"
#include "engine/battle.h"
-#include "game/area.h"
#include "game/content.h"
#include "config.h"
@@ -28,6 +27,17 @@ ph_area_catalog_test(void)
.markers = marker_defs,
.marker_count = (int)LEN(marker_defs),
};
+ const PhEntityDef entity_defs[] = {
+ { .id = 10, .stats = { .max_hp = 10 }, .kind = PH_ENTITY_PLAYER },
+ { .id = 20, .stats = { .max_hp = 5 }, .kind = PH_ENTITY_MONSTER },
+ };
+ const PhItemDef item_defs[] = { { .id = 30 } };
+ const PhWorldCatalog content = {
+ .entities = entity_defs,
+ .items = item_defs,
+ .entity_count = (int)LEN(entity_defs),
+ .item_count = (int)LEN(item_defs),
+ };
PhArea area = {
.name = "Catalog",
.width = 2,
@@ -41,16 +51,11 @@ ph_area_catalog_test(void)
int entity_count;
int item_count;
- ph_world_init(&world, area, 7, 32.0f, 32.0f);
- if (ph_world_add_entity_def(&world, (PhEntityDef){
- .id = 10, .stats = { .max_hp = 10 }, .kind = PH_ENTITY_PLAYER }) < 0 ||
- ph_world_add_entity_def(&world, (PhEntityDef){
- .id = 20, .stats = { .max_hp = 5 }, .kind = PH_ENTITY_MONSTER }) < 0 ||
- ph_world_add_item_def(&world, (PhItemDef){ .id = 30 }) < 0 ||
+ if (ph_world_init(&world, area, 7, content, 32.0f, 32.0f) < 0 ||
ph_world_prepare_area(&world, &area, &catalog, 7, &player) < 0)
return 1;
if (player < 0 || world.entity_count != 2 || world.ground_item_count != 1 ||
- world.entities[1].territory_radius != 3 ||
+ world.entities[1].behavior.territory_radius != 3 ||
world.ground_items[0].amount != 2 || world.initialized_area_count != 1)
return 1;
entity_count = world.entity_count;
@@ -75,6 +80,17 @@ ph_movement_test(int open, int blocked)
WANDER_ACTIONS = 20,
};
unsigned char tiles[MAP_W * MAP_H];
+ PhEntityDef entity_defs[] = {
+ { .id = 1, .stats = { .max_hp = 10 }, .move_speed = 2,
+ .animation_frames = 4, .kind = PH_ENTITY_PLAYER },
+ { .id = 2, .stats = { .max_hp = 10 }, .move_speed = 1,
+ .vision = 8, .aggressive = 1, .blocks_movement = 1,
+ .kind = PH_ENTITY_MONSTER },
+ };
+ const PhWorldCatalog content = {
+ .entities = entity_defs,
+ .entity_count = (int)LEN(entity_defs),
+ };
PhArea area = { .name = "Movement", .width = MAP_W, .height = MAP_H,
.tiles = tiles, .tile_defs = ph_tile_defs,
.tile_def_count = (int)LEN(ph_tile_defs) };
@@ -95,69 +111,63 @@ ph_movement_test(int open, int blocked)
for (x = 0; x < area.width; ++x)
if (!x || !y || x == area.width - 1 || y == area.height - 1)
tiles[y * area.width + x] = (unsigned char)blocked;
- ph_world_init(&world, area, 1, (float)(area.width * PH_TILE_SIZE),
- (float)(area.height * PH_TILE_SIZE));
- if (ph_world_add_entity_def(&world, (PhEntityDef){
- .id = 1, .stats = { .max_hp = 10 }, .move_speed = 2,
- .animation_frames = 4, .kind = PH_ENTITY_PLAYER }) < 0 ||
- ph_world_add_entity_def(&world, (PhEntityDef){
- .id = 2, .stats = { .max_hp = 10 }, .move_speed = 1,
- .vision = 8, .aggressive = 1, .blocks_movement = 1,
- .kind = PH_ENTITY_MONSTER }) < 0 ||
+ if (ph_world_init(&world, area, 1, content,
+ (float)(area.width * PH_TILE_SIZE),
+ (float)(area.height * PH_TILE_SIZE)) < 0 ||
(player_index = ph_world_spawn_entity(&world, 1, player_start, 0, 1)) < 0 ||
(mob_index = ph_world_spawn_entity(&world, 2, mob_start,
TERRITORY, 1)) < 0) return 1;
ph_world_set_player(&world, player_index);
mob = &world.entities[mob_index];
ph_world_tick(&world, (PhInput){ 0 }, action_seconds);
- if (!mob->moving || mob->move_target.x != PH_TILE_CENTER(MOB_COL - 1) ||
- mob->move_target.y != mob_start.y) return 1;
+ if (!mob->motion.moving || mob->motion.target.x != PH_TILE_CENTER(MOB_COL - 1) ||
+ mob->motion.target.y != mob_start.y) return 1;
ph_world_tick(&world, (PhInput){ 0 }, PH_MAX_FRAME_TIME);
if (mob->pos.x >= mob_start.x) return 1;
ph_world_tick(&world, (PhInput){ .move_x = 1 }, PH_MAX_FRAME_TIME);
- if (!world.entities[player_index].moving ||
- world.entities[player_index].move_target.x !=
+ if (!world.entities[player_index].motion.moving ||
+ world.entities[player_index].motion.target.x !=
PH_TILE_CENTER(PLAYER_COL + 1) ||
- ph_entity_animation_frame(&world.entity_defs[0],
+ ph_entity_animation_frame(&world.content.entities[0],
&world.entities[player_index]) != 0) return 1;
ph_world_tick(&world, (PhInput){ 0 }, PH_MAX_FRAME_TIME);
- if (ph_entity_animation_frame(&world.entity_defs[0],
+ if (ph_entity_animation_frame(&world.content.entities[0],
&world.entities[player_index]) <= 0) return 1;
world.entities[player_index].pos =
- world.entities[player_index].move_target = player_start;
- world.entities[player_index].moving = 0;
- mob->pos = mob->move_target = (PhVec2){
+ world.entities[player_index].motion.target = player_start;
+ world.entities[player_index].motion.moving = 0;
+ mob->pos = mob->motion.target = (PhVec2){
PH_TILE_CENTER(PLAYER_COL + 1), player_start.y };
- mob->moving = 0;
- mob->move_credit = 0.0f;
+ mob->motion.moving = 0;
+ mob->motion.credit = 0.0f;
ph_world_tick(&world, (PhInput){ 0 }, action_seconds);
if (world.encounter_index != mob_index) return 1;
world.entities[player_index].pos = player_start;
- mob->pos = mob->move_target = mob_start;
- mob->last_seen = player_start;
- mob->moving = 0;
- mob->move_credit = 0.0f;
- mob->chase_turns = 2;
+ mob->pos = mob->motion.target = mob_start;
+ mob->behavior.last_seen = player_start;
+ mob->motion.moving = 0;
+ mob->motion.credit = 0.0f;
+ mob->behavior.chase_turns = 2;
world.encounter_index = -1;
tiles[ROW * area.width + WALL_COL] = (unsigned char)blocked;
ph_world_tick(&world, (PhInput){ 0 }, action_seconds);
- if (mob->moving || mob->chase_turns != 1) return 1;
+ if (mob->motion.moving || mob->behavior.chase_turns != 1) return 1;
tiles[ROW * area.width + WALL_COL] = (unsigned char)open;
- world.entity_defs[1].aggressive = 0;
- mob->home = mob->pos = mob->move_target = mob_start;
- mob->territory_radius = 1;
- mob->chase_turns = 0;
- mob->move_credit = 0.0f;
+ entity_defs[1].aggressive = 0;
+ mob->behavior.home = mob->pos = mob->motion.target = mob_start;
+ mob->behavior.territory_radius = 1;
+ mob->behavior.chase_turns = 0;
+ mob->motion.credit = 0.0f;
for (i = 0; i < WANDER_ACTIONS; ++i) {
float dx;
float dy;
- float radius = (float)(mob->territory_radius * PH_TILE_SIZE);
+ float radius = (float)(mob->behavior.territory_radius * PH_TILE_SIZE);
ph_world_tick(&world, (PhInput){ 0 }, action_seconds);
- dx = mob->pos.x - mob->home.x;
- dy = mob->pos.y - mob->home.y;
+ dx = mob->pos.x - mob->behavior.home.x;
+ dy = mob->pos.y - mob->behavior.home.y;
if (dx != 0.0f || dy != 0.0f) moved = 1;
if (dx * dx + dy * dy > radius * radius) return 1;
}
@@ -169,6 +179,33 @@ ph_logic_test(int open, int blocked)
{
enum { MAP_W = 4, MAP_H = 3, ROW = 1, LEFT_COL = 1, RIGHT_COL = 2 };
unsigned char tiles[MAP_W * MAP_H];
+ const PhEntityDef entity_defs[] = {
+ {
+ .id = 1, .name = "Tester",
+ .stats = { .max_hp = 5, .max_mp = 5, .strength = 4,
+ .magic = 3, .accuracy = 100, .agility = 1 },
+ .move_speed = 1, .kind = PH_ENTITY_PLAYER,
+ },
+ {
+ .id = 4, .name = "Target",
+ .stats = { .max_hp = 8, .strength = 2, .defense = 1,
+ .accuracy = 100 },
+ .reward = { 7, 2, 1 },
+ .blocks_movement = 1, .kind = PH_ENTITY_MONSTER,
+ },
+ };
+ const PhItemDef item_defs[] = {
+ { .id = 1, .bonuses = { .max_hp = 2, .strength = 2 },
+ .equip_slot = PH_EQUIP_WEAPON },
+ { .id = 2, .use = PH_ITEM_USE_HEAL, .use_power = 3 },
+ { .id = 3, .equip_slot = PH_EQUIP_WEAPON },
+ };
+ const PhWorldCatalog content = {
+ .entities = entity_defs,
+ .items = item_defs,
+ .entity_count = (int)LEN(entity_defs),
+ .item_count = (int)LEN(item_defs),
+ };
PhArea area = { .name = "Smoke", .width = MAP_W, .height = MAP_H,
.tiles = tiles, .tile_defs = ph_tile_defs,
.tile_def_count = (int)LEN(ph_tile_defs) };
@@ -186,24 +223,9 @@ ph_logic_test(int open, int blocked)
memset(tiles, blocked, sizeof(tiles));
tiles[ROW * area.width + LEFT_COL] = (unsigned char)open;
tiles[ROW * area.width + RIGHT_COL] = (unsigned char)open;
- ph_world_init(&world, area, 1, (float)(area.width * PH_TILE_SIZE),
- (float)(area.height * PH_TILE_SIZE));
- 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, .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, .accuracy = 100 },
- .xp_reward = 7, .loot_item_id = 2, .loot_amount = 1,
- .blocks_movement = 1, .kind = PH_ENTITY_MONSTER }) < 0 ||
- ph_world_add_item_def(&world, (PhItemDef){ .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 ||
- ph_world_add_item_def(&world, (PhItemDef){ .id = 3,
- .equip_slot = PH_EQUIP_WEAPON }) < 0 ||
+ if (ph_world_init(&world, area, 1, content,
+ (float)(area.width * PH_TILE_SIZE),
+ (float)(area.height * PH_TILE_SIZE)) < 0 ||
(player_index = ph_world_spawn_entity(&world, 1, player_start, 0, 1)) < 0 ||
(enemy_index = ph_world_spawn_entity(&world, 4, enemy_start, 0, 1)) < 0 ||
ph_world_drop_item(&world, 1, player_start, 1, 1) < 0 ||
@@ -216,7 +238,7 @@ ph_logic_test(int open, int blocked)
ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f);
if (!world.ground_items[2].active) return 1;
if (ph_world_equip_inventory_item(&world, player_index, 1) < 0) return 1;
- world.entities[player_index].hp = 1;
+ world.entities[player_index].vitals.hp = 1;
if (ph_world_use_inventory_item(&world, player_index, 2) < 0 ||
ph_world_drop_inventory_item(&world, player_index, 2) < 0) return 1;
@@ -226,11 +248,11 @@ ph_logic_test(int open, int blocked)
stats = ph_world_entity_stats(&world, player);
if (!player || (int)(player->pos.x / PH_TILE_SIZE) != RIGHT_COL ||
(int)(player->pos.y / PH_TILE_SIZE) != ROW ||
- player->facing_x != 0 || player->facing_y != 1 ||
+ player->motion.facing_x != 0 || player->motion.facing_y != 1 ||
world.camera.pos.x != 0.0f || world.camera.pos.y != 0.0f ||
ph_world_item_amount(&world, 1) != 0 ||
ph_world_item_amount(&world, 2) != 0 ||
- ph_world_item_amount(&world, 3) != 1 || player->hp != 4 ||
+ ph_world_item_amount(&world, 3) != 1 || player->vitals.hp != 4 ||
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)
@@ -241,16 +263,16 @@ ph_logic_test(int open, int blocked)
ph_battle_begin(&battle, &world, enemy_index, ph_player_spellbook);
if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) !=
PH_BATTLE_READY) return 1;
- battle.command = PH_BATTLE_FIGHT;
+ battle.choice.command = PH_BATTLE_FIGHT;
ph_battle_order(&battle, &world, order);
if (order[0] != player_index || order[1] != enemy_index) return 1;
- battle.command = PH_BATTLE_MAGICK;
+ battle.choice.command = PH_BATTLE_MAGICK;
ph_battle_order(&battle, &world, order);
if (order[0] != player_index || order[1] != enemy_index) return 1;
ph_battle_select(&battle, &world);
ph_battle_order(&battle, &world, order);
if (battle.phase != PH_BATTLE_MAGIC || order[0] != player_index ||
- order[1] != player_index || world.entities[player_index].mp != 5 ||
+ order[1] != player_index || world.entities[player_index].vitals.mp != 5 ||
world.encounter_index != enemy_index ||
ph_world_apply_accuracy_penalty(&world, enemy_index, 20, 3) < 0)
return 1;
@@ -261,16 +283,16 @@ ph_logic_test(int open, int blocked)
ph_world_advance_effects(&world, enemy_index);
ph_world_advance_effects(&world, enemy_index);
ph_world_advance_effects(&world, enemy_index);
- if (world.entities[enemy_index].accuracy_penalty != 0 ||
- world.entities[enemy_index].accuracy_turns != 0 ||
+ if (world.entities[enemy_index].accuracy.value != 0 ||
+ world.entities[enemy_index].accuracy.turns != 0 ||
ph_world_physical_attack(&world, player_index, enemy_index) <= 0 ||
ph_world_magic_attack(&world, player_index, enemy_index, 2, 4) <= 0 ||
- world.entities[enemy_index].hp != 0 ||
+ world.entities[enemy_index].vitals.hp != 0 ||
ph_world_finish_encounter(&world, enemy_index) < 0 ||
- player->xp != 7 || player->mp != 3 ||
+ player->xp != 7 || player->vitals.mp != 3 ||
ph_world_item_amount(&world, 2) != 1 || world.encounter_index != -1)
return 1;
- printf("smoke hp=%d equipment=%d xp=%d\n", player->hp,
+ printf("smoke hp=%d equipment=%d xp=%d\n", player->vitals.hp,
player->equipment[PH_EQUIP_WEAPON], player->xp);
return 0;
}
@@ -283,7 +305,7 @@ ph_enter_portal(PhWorld *world)
for (i = 0; i < world->area.marker_count; ++i) {
const PhAreaMarker *marker = &world->area.markers[i];
- const PhMarkerDef *def = ph_area_catalog_marker(&ph_area_catalog,
+ const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas,
world->area_id, marker->symbol);
const PhTileDef *tile;
@@ -292,7 +314,7 @@ ph_enter_portal(PhWorld *world)
if (!tile || tile->symbol != 'T') return -1;
player->pos = (PhVec2){ PH_TILE_CENTER(marker->tile_x),
PH_TILE_CENTER(marker->tile_y) };
- return ph_world_update_portal(world, &ph_area_catalog);
+ return ph_world_update_portal(world, ph_game_world.areas);
}
return -1;
}
@@ -309,7 +331,7 @@ ph_area_shop_test(void)
int shop_id = -1;
int i;
- if (ph_game_world_init(&world, 320.0f, 240.0f) < 0) return 1;
+ if (ph_world_start(&world, &ph_game_world, 320.0f, 240.0f) < 0) return 1;
player = &world.entities[world.player_index];
potion = ph_world_item_def(&world, PH_ITEM_HEALING_POTION);
ether = ph_world_item_def(&world, PH_ITEM_ETHER);
@@ -332,8 +354,8 @@ ph_area_shop_test(void)
shop_id = interaction->content_id;
player->pos = (PhVec2){ world.entities[i].pos.x,
world.entities[i].pos.y + PH_TILE_SIZE };
- player->facing_x = 0;
- player->facing_y = -1;
+ player->motion.facing_x = 0;
+ player->motion.facing_y = -1;
break;
}
}
@@ -353,9 +375,9 @@ ph_area_shop_test(void)
ph_world_sell_item(&world, world.player_index, potion->id) < 0 ||
player->gold != 40 - potion->buy_value + potion->sell_value) goto fail;
if (ph_world_buy_item(&world, world.player_index, ether->id) < 0) goto fail;
- player->mp = 0;
+ player->vitals.mp = 0;
if (ph_world_use_inventory_item(&world, world.player_index, ether->id) < 0 ||
- player->mp != ether->use_power) goto fail;
+ player->vitals.mp != ether->use_power) goto fail;
if (ph_enter_portal(&world) != 1 || world.area_id != PH_AREA_MEADOW ||
world.initialized_area_count != 2)
goto fail;