phantasia

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

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:
MMakefile | 1-
MREADME.adoc | 189++++++++++++++++++++++++-------------------------------------------------------
Msrc/engine/area.c | 31+++++++++++++++++++++++++++++++
Msrc/engine/area.h | 14++++++++++++--
Msrc/engine/battle.c | 101+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/engine/battle.h | 21+++++++++++++--------
Msrc/engine/render.c | 20++++++++++----------
Msrc/engine/render.h | 10+++++++---
Msrc/engine/world.c | 236+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/engine/world.h | 80++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Dsrc/game/area.c | 34----------------------------------
Dsrc/game/area.h | 8--------
Msrc/game/content.c | 45+++++++++++++++++++++++++++++++--------------
Msrc/game/content.h | 3+--
Msrc/game/main.c | 10+++++-----
Msrc/game/presentation.c | 62++++++++++++++++++++++++++++++++------------------------------
Msrc/tests/render.c | 35++++++++++++++++++-----------------
Msrc/tests/smoke.c | 178++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
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, &notice_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;