phantasia

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

commit 4d71e647f0f4daa1d931fe03eb6a0d9d32345e56
parent 45aaceae9fef8bb703738666cd40c320363d17d0
Author: beep <beep@wimdupont.com>
Date:   Wed, 29 Jul 2026 14:49:33 +0000

Polish pickups, movement, and town tiles

Restrict pickups to the player's tile, calm distance-based walk animation with pixel-snapped rendering, and construct Stonehollow buildings from proper wall, floor, and doorway tiles.

Diffstat:
Mconfig.def.h | 4++--
Mdata/maps/stonehollow.txt | 20++++++++++----------
Msrc/engine/world.c | 40+++++++++++++++++++++++++++-------------
Msrc/engine/world.h | 3++-
Msrc/game/content.c | 12++++++++++--
Msrc/game/content.h | 2+-
Msrc/game/main.c | 36++++++++++++++++++++++++++----------
Msrc/tests/smoke.c | 13+++++++++++--
8 files changed, 89 insertions(+), 41 deletions(-)

diff --git a/config.def.h b/config.def.h @@ -1,7 +1,7 @@ #ifndef CONFIG_H #define CONFIG_H -#define PH_CONFIG_VERSION 8 +#define PH_CONFIG_VERSION 9 #define PH_VIEW_W 320 #define PH_VIEW_H 240 @@ -11,10 +11,10 @@ #define PH_FRAME_DELAY_MS 16 #define PH_ENTITY_BLOCK_RADIUS 10 -#define PH_PICKUP_RADIUS 18.0f #define PH_INTERACT_RADIUS 20.0f #define PH_NOTICE_SECONDS 1.25f #define PH_WORLD_STEP_MILLISECONDS 250 +#define PH_WORLD_ANIMATION_PIXELS_PER_FRAME 8 #define PH_WORLD_ACTION_MILLISECONDS 500 #define PH_MOB_MEMORY_TURNS 4 #define PH_MOB_WANDER_PERCENT 60 diff --git a/data/maps/stonehollow.txt b/data/maps/stonehollow.txt @@ -2,19 +2,19 @@ name: Stonehollow ################################ #..............................# -#..BBBBBBBB....BBBBBBBB........# -#..B======B....B======B........# -#..B======B....B======B........# -#..B======B....B======B........# -#..BBB=BBBB....BBBB=BBB........# +#..[^^^^^^]....[^^^^^^]........# +#..{======}....{======}........# +#..{======}....{======}........# +#..{======}....{======}........# +#..(__+___)....(___+__)........# #.....,............,...........# #.....,,,,,,,,,,,,,,...........# #...........,..................# -#..BBBBBBBB.,..BBBBBBBB........# -#..B======B.,..B======B........# -#..B======B.,..B======B........# -#..B======B.,..B======B........# -#..BBB=BBBB.,..BBB=BBBB........# +#..[^^^^^^].,..[^^^^^^]........# +#..{======}.,..{======}........# +#..{======}.,..{======}........# +#..{======}.,..{======}........# +#..(__+___).,..(__+___)........# #.....,.....,.....,............# #.....,,,,,,,,,,,,,............# #...........,..................# diff --git a/src/engine/world.c b/src/engine/world.c @@ -12,7 +12,8 @@ #error "combat formula divisors must be positive" #endif -#if PH_WORLD_STEP_MILLISECONDS <= 0 || PH_WORLD_ACTION_MILLISECONDS <= 0 || \ +#if PH_WORLD_STEP_MILLISECONDS <= 0 || PH_WORLD_ANIMATION_PIXELS_PER_FRAME <= 0 || \ + PH_WORLD_ACTION_MILLISECONDS <= 0 || \ PH_MOB_MEMORY_TURNS < 0 || \ PH_MOB_WANDER_PERCENT < 0 || PH_MOB_WANDER_PERCENT > 100 #error "invalid world movement configuration" @@ -212,7 +213,10 @@ ph_try_pickup(PhWorld *world, PhEntity *entity) if (!drop->active || drop->area_id != world->area_id) { continue; } - if (ph_dist2(entity->pos, drop->pos) > PH_PICKUP_RADIUS * PH_PICKUP_RADIUS) { + if ((int)floorf(entity->pos.x / PH_TILE_SIZE) != + (int)floorf(drop->pos.x / PH_TILE_SIZE) || + (int)floorf(entity->pos.y / PH_TILE_SIZE) != + (int)floorf(drop->pos.y / PH_TILE_SIZE)) { continue; } @@ -383,6 +387,9 @@ ph_world_advance_movers(PhWorld *world, float dt) float dy; float distance; float travel; + float moved; + float animation_cycle; + int animation_frames; if (!entity->active || !entity->moving || entity->area_id != world->area_id) continue; @@ -392,6 +399,16 @@ ph_world_advance_movers(PhWorld *world, float dt) dy = entity->move_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; + 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, + animation_cycle); if (distance <= travel || distance <= 0.001f) { entity->pos = entity->move_target; entity->moving = 0; @@ -855,6 +872,7 @@ ph_world_tick(PhWorld *world, PhInput input, float dt) { PhEntity *player; const PhEntityDef *def; + int was_moving; if (world->player_index < 0 || world->player_index >= world->entity_count) { return; @@ -875,10 +893,15 @@ ph_world_tick(PhWorld *world, PhInput input, float dt) input = ph_orthogonal_input(input); world->interaction_id = 0; + was_moving = player->moving; if (!player->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 && + def->move_speed > 0) + ph_entity_begin_step(world, world->player_index, + input.move_x, input.move_y, 0); if (input.interact) { ph_try_pickup(world, player); @@ -892,22 +915,13 @@ ph_world_tick(PhWorld *world, PhInput input, float dt) int ph_entity_animation_frame(const PhEntityDef *def, const PhEntity *entity) { - float dx; - float dy; - float remaining; - float progress; int frames; - int frame; if (!def || !entity || !entity->moving) return 0; frames = def->animation_frames > 0 && def->animation_frames <= PH_ANIMATION_FRAMES ? def->animation_frames : 1; - dx = entity->move_target.x - entity->pos.x; - dy = entity->move_target.y - entity->pos.y; - remaining = sqrtf(dx * dx + dy * dy); - progress = 1.0f - ph_clampf(remaining / (float)PH_TILE_SIZE, 0.0f, 1.0f); - frame = (int)(progress * (float)frames); - return frame < frames ? frame : frames - 1; + return (int)(entity->animation_distance / + PH_WORLD_ANIMATION_PIXELS_PER_FRAME) % frames; } const PhEntity * diff --git a/src/engine/world.h b/src/engine/world.h @@ -3,7 +3,7 @@ #include <stddef.h> -#define PH_CONFIG_API_VERSION 8 +#define PH_CONFIG_API_VERSION 9 enum { PH_MAP_MAGIC = 0x50484d50, @@ -126,6 +126,7 @@ typedef struct { int chase_turns; int moving; float move_credit; + float animation_distance; int equipment[PH_EQUIP_COUNT]; int active; } PhEntity; diff --git a/src/game/content.c b/src/game/content.c @@ -16,8 +16,16 @@ const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = { { '.', 0, PH_ASSET_TILESET, 1, 1 }, { '#', 1, PH_ASSET_TILESET, 1, 5 }, { ',', 0, PH_ASSET_TILESET, 5, 0 }, - { 'B', 1, PH_ASSET_TILESET, 1, 16 }, - { '=', 0, PH_ASSET_TILESET, 3, 14 }, + { '[', 1, PH_ASSET_TILESET, 9, 1 }, + { '^', 1, PH_ASSET_TILESET, 10, 1 }, + { ']', 1, PH_ASSET_TILESET, 11, 1 }, + { '{', 1, PH_ASSET_TILESET, 9, 2 }, + { '=', 0, PH_ASSET_TILESET, 10, 2 }, + { '}', 1, PH_ASSET_TILESET, 11, 2 }, + { '(', 1, PH_ASSET_TILESET, 9, 3 }, + { '_', 1, PH_ASSET_TILESET, 10, 3 }, + { ')', 1, PH_ASSET_TILESET, 11, 3 }, + { '+', 0, PH_ASSET_TILESET, 10, 3 }, { 'T', 0, PH_ASSET_TILESET, 6, 11 }, }; diff --git a/src/game/content.h b/src/game/content.h @@ -114,7 +114,7 @@ enum { }; enum { - PH_TILE_DEF_COUNT = 6, + PH_TILE_DEF_COUNT = 14, PH_ENTITY_SPAWN_COUNT = 3, PH_ITEM_DROP_COUNT = 2, PH_PORTAL_COUNT = 2, diff --git a/src/game/main.c b/src/game/main.c @@ -3,6 +3,7 @@ #include "game/battle.h" #include "game/content.h" +#include <math.h> #include <stdbool.h> #include <stdio.h> #include <string.h> @@ -106,6 +107,12 @@ ph_ui_center_text(const char *text) } static float +ph_world_pixel(float position, float camera) +{ + return floorf(position + 0.5f) - floorf(camera + 0.5f); +} + +static float ph_draw_wrapped_text(SDL_Renderer *renderer, float x, float y, float width, float line_height, const char *text) { @@ -231,8 +238,8 @@ ph_draw_area(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *worl const PhTileDef *def = ph_area_tile_def(&world->area, tx, ty); SDL_Texture *texture; SDL_FRect rect = { - .x = (float)(tx * PH_TILE_SIZE) - world->camera.pos.x, - .y = (float)(ty * PH_TILE_SIZE) - world->camera.pos.y, + .x = ph_world_pixel((float)(tx * PH_TILE_SIZE), world->camera.pos.x), + .y = ph_world_pixel((float)(ty * PH_TILE_SIZE), world->camera.pos.y), .w = (float)PH_TILE_SIZE, .h = (float)PH_TILE_SIZE, }; @@ -269,8 +276,8 @@ ph_draw_items(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *wor continue; } - rect.x = drop->pos.x - world->camera.pos.x - 3.0f; - rect.y = drop->pos.y - world->camera.pos.y - 3.0f; + rect.x = ph_world_pixel(drop->pos.x, world->camera.pos.x) - 3.0f; + rect.y = ph_world_pixel(drop->pos.y, world->camera.pos.y) - 3.0f; rect.w = 6.0f; rect.h = 6.0f; @@ -278,8 +285,10 @@ ph_draw_items(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld *wor (size_t)def->asset_id < LEN(assets->textures)) texture = assets->textures[def->asset_id]; if (texture && def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0) { - rect.x = drop->pos.x - world->camera.pos.x - PH_TILE_SIZE * 0.5f; - rect.y = drop->pos.y - world->camera.pos.y - PH_TILE_SIZE * 0.5f; + rect.x = ph_world_pixel(drop->pos.x, world->camera.pos.x) - + PH_TILE_SIZE * 0.5f; + rect.y = ph_world_pixel(drop->pos.y, world->camera.pos.y) - + PH_TILE_SIZE * 0.5f; rect.w = PH_TILE_SIZE; rect.h = PH_TILE_SIZE; ph_draw_sprite(renderer, texture, @@ -315,15 +324,17 @@ ph_draw_entities(SDL_Renderer *renderer, const PhAssets *assets, const PhWorld * ph_entity_sprite_frame(def, entity, &sprite_tile_x, &sprite_tile_y, &flip); if (texture && sprite_tile_x >= 0 && sprite_tile_y >= 0) { - rect.x = entity->pos.x - world->camera.pos.x - PH_TILE_SIZE * 0.5f; - rect.y = entity->pos.y - world->camera.pos.y - PH_TILE_SIZE * 0.75f; + rect.x = ph_world_pixel(entity->pos.x, world->camera.pos.x) - + PH_TILE_SIZE * 0.5f; + rect.y = ph_world_pixel(entity->pos.y, world->camera.pos.y) - + PH_TILE_SIZE * 0.75f; rect.w = PH_TILE_SIZE; rect.h = PH_TILE_SIZE; ph_draw_sprite(renderer, texture, sprite_tile_x, sprite_tile_y, rect, flip); } else { - rect.x = entity->pos.x - world->camera.pos.x - 7.0f; - rect.y = entity->pos.y - world->camera.pos.y - 12.0f; + rect.x = ph_world_pixel(entity->pos.x, world->camera.pos.x) - 7.0f; + rect.y = ph_world_pixel(entity->pos.y, world->camera.pos.y) - 12.0f; rect.w = 14.0f; rect.h = 18.0f; @@ -1203,6 +1214,11 @@ ph_run_render_smoke_test(void) return 1; } + ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); + if (ph_world_item_amount(&world, ph_item_drops[0].item_id) != 0) { + fprintf(stderr, "render smoke test failed: adjacent item pickup\n"); + result = 1; + } ph_world_tick(&world, (PhInput){ .move_x = 1 }, (float)PH_WORLD_STEP_MILLISECONDS / 1000.0f); ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); diff --git a/src/tests/smoke.c b/src/tests/smoke.c @@ -65,7 +65,9 @@ ph_movement_test(int open, int blocked) ph_world_tick(&world, (PhInput){ .move_x = 1 }, PH_MAX_FRAME_TIME); if (!world.entities[player_index].moving || world.entities[player_index].move_target.x != - PH_TILE_CENTER(PLAYER_COL + 1)) return 1; + PH_TILE_CENTER(PLAYER_COL + 1) || + ph_entity_animation_frame(&world.entity_defs[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], &world.entities[player_index]) <= 0) return 1; @@ -153,10 +155,14 @@ ph_logic_test(int open, int blocked) (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 || - ph_world_drop_item(&world, 2, player_start, 2, 1) < 0) return 1; + ph_world_drop_item(&world, 2, player_start, 2, 1) < 0 || + ph_world_drop_item(&world, 1, + (PhVec2){ PH_TILE_CENTER(RIGHT_COL), PH_TILE_CENTER(ROW) }, + 1, 1) < 0) return 1; ph_world_set_player(&world, player_index); if (ph_world_equip_item(&world, player_index, 3) < 0) return 1; 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; if (ph_world_use_inventory_item(&world, player_index, 2) < 0 || @@ -237,6 +243,9 @@ ph_area_shop_test(void) player->pos = (PhVec2){ PH_TILE_CENTER(29), PH_TILE_CENTER(2) }; if (ph_game_update_portal(&world) != 1 || world.area_id != PH_AREA_STONEHOLLOW) goto fail; + if (!ph_area_tile_blocked(&world.area, 3, 2) || + ph_area_tile_blocked(&world.area, 4, 3) || + ph_area_tile_blocked(&world.area, 6, 6)) goto fail; for (i = 0; i < world.entity_count; ++i) { const PhEntityDef *def = ph_world_entity_def(&world, world.entities[i].type_id);