commit 0ad328617881fd26dd8bafb5d661ac05fd94ed3c
parent d1044343c4931528574cacee81ecd8fc4c88aaac
Author: beep <beep@wimdupont.com>
Date: Thu, 30 Jul 2026 10:37:29 +0000
Improve combat targeting and world landmarks
Diffstat:
9 files changed, 115 insertions(+), 50 deletions(-)
diff --git a/Makefile b/Makefile
@@ -86,9 +86,9 @@ $(MAPDIR)/%.phmap: data/maps/%.txt $(MAPC)
@mkdir -p $(dir $@)
$(MAPC) $< $@
-$(TILESET_BMP): $(TILESET_SRC)
+$(TILESET_BMP): $(TILESET_SRC) Makefile
@mkdir -p $(dir $@)
- magick $< -alpha off BMP3:$@
+ magick $< -background black -alpha remove -alpha off BMP3:$@
$(HERO_BMP): $(HERO_SRC)
@mkdir -p $(dir $@)
diff --git a/data/maps/ashen-meadow.txt b/data/maps/ashen-meadow.txt
@@ -1,10 +1,10 @@
name: Ashen Meadow
################################
-#:..;...:.....;..:.....;.......#
-#..:....;..............:....e>.#
-#............;......:..........#
-#....@1..........:.......;.....#
+#:..;...:.....;..:.....;..VVVV.#
+#..:....;..............:..VVVV.#
+#............;......:.....VV>V.#
+#....@1..........:.......;..e..#
#.....2.....:........###.......#
#..;.......a.........###..:....#
#........:...........###.......#
diff --git a/src/engine/battle.c b/src/engine/battle.c
@@ -267,32 +267,17 @@ ph_battle_cast(PhBattle *battle, PhWorld *world)
snprintf(battle->message, sizeof(battle->message), "Choose a target.");
}
-static PhSpellTarget
-ph_battle_target_kind(const PhBattle *battle)
-{
- const PhSpellDef *spell;
-
- if (battle->choice.command == PH_BATTLE_FIGHT)
- return PH_SPELL_TARGET_OPPONENT;
- if (battle->choice.command == PH_BATTLE_ITEM)
- return PH_SPELL_TARGET_ALLY;
- spell = ph_battle_spell(battle);
- return spell ? spell->target : PH_SPELL_TARGET_NONE;
-}
-
static int
ph_battle_target_valid(const PhBattle *battle, const PhWorld *world, int index)
{
const PhEntityDef *def;
- PhSpellTarget target = ph_battle_target_kind(battle);
if (index < 0 || index >= world->entity_count ||
!world->entities[index].active || world->entities[index].vitals.hp <= 0)
return 0;
- if (target == PH_SPELL_TARGET_OPPONENT) return index == battle->enemy_index;
+ if (index == battle->enemy_index) return 1;
def = ph_world_entity_def(world, world->entities[index].type_id);
- return target == PH_SPELL_TARGET_ALLY && def &&
- def->kind == PH_ENTITY_PLAYER &&
+ return def && def->kind == PH_ENTITY_PLAYER &&
world->entities[index].area_id == world->area_id;
}
@@ -381,7 +366,8 @@ ph_battle_resolve(PhBattle *battle, PhWorld *world)
int damage;
if (battle->phase == PH_BATTLE_PLAYER_ACTION) {
- def = ph_world_entity_def(world, world->entities[battle->enemy_index].type_id);
+ def = ph_world_entity_def(world,
+ world->entities[battle->choice.target_index].type_id);
if (battle->choice.command == PH_BATTLE_FIGHT) {
damage = ph_world_physical_attack(world, world->player_index,
battle->choice.target_index);
@@ -399,14 +385,14 @@ ph_battle_resolve(PhBattle *battle, PhWorld *world)
damage = ph_spell_heal(world, world->player_index,
battle->choice.target_index, spell);
snprintf(battle->message, sizeof(battle->message),
- damage >= 0 ? "%s restores %d HP." : "%s has no effect.",
- spell->name, damage);
+ damage >= 0 ? "%s recovers %d HP." : "%s is unaffected.",
+ def && def->name ? def->name : "Target", damage);
return;
} else return;
} else {
const PhItemDef *item = ph_world_item_def(world, battle->choice.item_id);
- damage = ph_world_use_inventory_item(world, world->player_index,
+ damage = ph_world_use_inventory_item(world, battle->choice.target_index,
battle->choice.item_id) < 0 ? -1 : 0;
snprintf(battle->message, sizeof(battle->message), "Used %s.",
item && item->name ? item->name : "item");
diff --git a/src/engine/render.c b/src/engine/render.c
@@ -156,6 +156,32 @@ ph_draw_area(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld
sprite_tile_x += joins_west == joins_east ? 1 : joins_west ? 2 : 0;
sprite_tile_y += joins_north == joins_south ? 1 : joins_north ? 2 : 0;
+ } else if (def->autotile == PH_TILE_AUTOTILE_FIXED &&
+ def->autotile_width > 0 && def->autotile_height > 0) {
+ const PhTileDef *neighbor;
+ int offset_x = 0;
+ int offset_y = 0;
+
+ while (offset_x < def->autotile_width - 1 &&
+ (neighbor = ph_area_tile_def(&world->area,
+ tx - offset_x - 1, ty)) &&
+ neighbor->autotile == def->autotile &&
+ neighbor->asset_id == def->asset_id &&
+ neighbor->sprite_tile_x == def->sprite_tile_x &&
+ neighbor->sprite_tile_y == def->sprite_tile_y &&
+ neighbor->autotile_width == def->autotile_width &&
+ neighbor->autotile_height == def->autotile_height) ++offset_x;
+ while (offset_y < def->autotile_height - 1 &&
+ (neighbor = ph_area_tile_def(&world->area,
+ tx, ty - offset_y - 1)) &&
+ neighbor->autotile == def->autotile &&
+ neighbor->asset_id == def->asset_id &&
+ neighbor->sprite_tile_x == def->sprite_tile_x &&
+ neighbor->sprite_tile_y == def->sprite_tile_y &&
+ neighbor->autotile_width == def->autotile_width &&
+ neighbor->autotile_height == def->autotile_height) ++offset_y;
+ sprite_tile_x += offset_x;
+ sprite_tile_y += offset_y;
}
src = (SDL_FRect){
.x = (float)(sprite_tile_x * PH_TILE_SIZE),
diff --git a/src/engine/world.h b/src/engine/world.h
@@ -113,12 +113,15 @@ typedef struct {
int background_sprite_tile_x;
int background_sprite_tile_y;
int autotile;
+ int autotile_width;
+ int autotile_height;
const char *interaction_notice;
} PhTileDef;
enum {
PH_TILE_AUTOTILE_NONE,
PH_TILE_AUTOTILE_3X3,
+ PH_TILE_AUTOTILE_FIXED,
};
typedef struct {
diff --git a/src/game/content.c b/src/game/content.c
@@ -15,13 +15,13 @@ const PhAreaDef ph_area_defs[PH_AREA_COUNT] = {
};
const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = {
- { '.', 0, PH_ASSET_TILESET, 1, 1, 0, 0, 0, 0, 0 },
- { ':', 0, PH_ASSET_TILESET, 1, 0, 0, 0, 0, 0, 0 },
- { ';', 0, PH_ASSET_TILESET, 2, 0, 0, 0, 0, 0, 0 },
- { ',', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0, 0, 0 },
- { 'T', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0, 0, 0 },
+ { '.', 0, PH_ASSET_TILESET, 1, 1, 0, 0, 0, 0, 0, 0, 0 },
+ { ':', 0, PH_ASSET_TILESET, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
+ { ';', 0, PH_ASSET_TILESET, 2, 0, 0, 0, 0, 0, 0, 0, 0 },
+ { ',', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0, 0, 0, 0, 0 },
+ { 'T', 0, PH_ASSET_TILESET, 6, 2, 0, 0, 0, 0, 0, 0, 0 },
{ '#', 1, PH_ASSET_TILESET, 9, 4, 0, 0, 0,
- PH_TILE_AUTOTILE_3X3, 0 },
+ PH_TILE_AUTOTILE_3X3, 0, 0, 0 },
{ .symbol = 'J', .blocks_movement = 1, .asset_id = PH_ASSET_TILESET,
.sprite_tile_x = 0, .sprite_tile_y = 16,
.background_asset_id = PH_ASSET_TILESET,
@@ -75,16 +75,28 @@ const PhTileDef ph_tile_defs[PH_TILE_DEF_COUNT] = {
.sprite_tile_x = 1, .sprite_tile_y = 16,
.background_asset_id = PH_ASSET_TILESET,
.background_sprite_tile_x = 1, .background_sprite_tile_y = 1 },
- { '[', 0, PH_ASSET_TILESET, 9, 1, 0, 0, 0, 0, 0 },
- { '^', 0, PH_ASSET_TILESET, 10, 1, 0, 0, 0, 0, 0 },
- { ']', 0, PH_ASSET_TILESET, 11, 1, 0, 0, 0, 0, 0 },
- { '{', 0, PH_ASSET_TILESET, 9, 2, 0, 0, 0, 0, 0 },
- { '=', 0, PH_ASSET_TILESET, 10, 2, 0, 0, 0, 0, 0 },
- { '}', 0, PH_ASSET_TILESET, 11, 2, 0, 0, 0, 0, 0 },
- { '(', 1, PH_ASSET_TILESET, 9, 3, 0, 0, 0, 0, 0 },
- { '_', 1, PH_ASSET_TILESET, 10, 3, 0, 0, 0, 0, 0 },
- { ')', 1, PH_ASSET_TILESET, 11, 3, 0, 0, 0, 0, 0 },
- { '+', 0, PH_ASSET_TILESET, 5, 15, 0, 0, 0, 0, 0 },
+ { '[', 0, PH_ASSET_TILESET, 9, 1, 0, 0, 0, 0, 0, 0, 0 },
+ { '^', 0, PH_ASSET_TILESET, 10, 1, 0, 0, 0, 0, 0, 0, 0 },
+ { ']', 0, PH_ASSET_TILESET, 11, 1, 0, 0, 0, 0, 0, 0, 0 },
+ { '{', 0, PH_ASSET_TILESET, 9, 2, 0, 0, 0, 0, 0, 0, 0 },
+ { '=', 0, PH_ASSET_TILESET, 10, 2, 0, 0, 0, 0, 0, 0, 0 },
+ { '}', 0, PH_ASSET_TILESET, 11, 2, 0, 0, 0, 0, 0, 0, 0 },
+ { '(', 1, PH_ASSET_TILESET, 9, 3, 0, 0, 0, 0, 0, 0, 0 },
+ { '_', 1, PH_ASSET_TILESET, 10, 3, 0, 0, 0, 0, 0, 0, 0 },
+ { ')', 1, PH_ASSET_TILESET, 11, 3, 0, 0, 0, 0, 0, 0, 0 },
+ { '+', 0, PH_ASSET_TILESET, 5, 15, 0, 0, 0, 0, 0, 0, 0 },
+ { .symbol = 'V', .blocks_movement = 1, .asset_id = PH_ASSET_TILESET,
+ .sprite_tile_x = 0, .sprite_tile_y = 13,
+ .background_asset_id = PH_ASSET_TILESET,
+ .background_sprite_tile_x = 1, .background_sprite_tile_y = 1,
+ .autotile = PH_TILE_AUTOTILE_FIXED,
+ .autotile_width = 4, .autotile_height = 3 },
+ { .symbol = 'v', .asset_id = PH_ASSET_TILESET,
+ .sprite_tile_x = 0, .sprite_tile_y = 13,
+ .background_asset_id = PH_ASSET_TILESET,
+ .background_sprite_tile_x = 1, .background_sprite_tile_y = 1,
+ .autotile = PH_TILE_AUTOTILE_FIXED,
+ .autotile_width = 4, .autotile_height = 3 },
};
const PhSpellDef ph_spell_defs[PH_SPELL_DEF_COUNT] = {
@@ -272,7 +284,7 @@ const PhMarkerDef ph_marker_defs[PH_MARKER_DEF_COUNT] = {
PH_ITEM_RUSTED_SPEAR, .amount = 1 },
{ PH_AREA_MEADOW, '2', '.', PH_MARKER_ITEM,
PH_ITEM_HEALING_POTION, .amount = 1 },
- { PH_AREA_MEADOW, '>', 'T', PH_MARKER_PORTAL,
+ { PH_AREA_MEADOW, '>', 'v', PH_MARKER_PORTAL,
.destination_area_id = PH_AREA_STONEHOLLOW, .destination_symbol = 'e' },
{ PH_AREA_MEADOW, 'e', '.', PH_MARKER_ARRIVAL, .content_id = 0 },
{ PH_AREA_STONEHOLLOW, '>', 'j', PH_MARKER_PORTAL,
diff --git a/src/game/content.h b/src/game/content.h
@@ -92,7 +92,7 @@ enum {
};
enum {
- PH_TILE_DEF_COUNT = 27,
+ PH_TILE_DEF_COUNT = 29,
PH_MARKER_DEF_COUNT = 18,
};
diff --git a/src/tests/render.c b/src/tests/render.c
@@ -8,6 +8,25 @@
#include "config.h"
#if PH_USE_SDL
+static int
+ph_render_enter_portal(PhWorld *world, int destination_area_id)
+{
+ int i;
+
+ for (i = 0; i < world->area.marker_count; ++i) {
+ const PhAreaMarker *marker = &world->area.markers[i];
+ const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas,
+ world->area_id, marker->symbol);
+
+ if (!def || def->kind != PH_MARKER_PORTAL ||
+ def->destination_area_id != destination_area_id) continue;
+ world->entities[world->player_index].pos = (PhVec2){
+ PH_TILE_CENTER(marker->tile_x), PH_TILE_CENTER(marker->tile_y) };
+ return ph_world_update_portal(world, ph_game_world.areas);
+ }
+ return -1;
+}
+
int
main(void)
{
@@ -219,19 +238,21 @@ main(void)
ph_present_frame(renderer, &assets, &context);
world.entities[enemy_index].vitals.hp =
ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp;
- world.entities[world.player_index].vitals.hp -= 18;
+ world.entities[enemy_index].vitals.hp = 1;
battle.choice.command = PH_BATTLE_MAGICK;
ph_battle_select(&battle, &world);
battle.choice.spell = 1;
ph_battle_cast(&battle, &world);
if (battle.phase != PH_BATTLE_TARGET ||
battle.choice.target_index != world.player_index) result = 1;
+ if (ph_battle_target_step(&battle, &world, 1) != enemy_index) result = 1;
+ ph_present_frame(renderer, &assets, &context);
ph_battle_confirm_target(&battle, &world);
ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
- if (ph_world_player(&world)->vitals.hp !=
- ph_world_entity_stats(&world, ph_world_player(&world)).max_hp ||
+ if (world.entities[enemy_index].vitals.hp !=
+ ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp ||
ph_world_player(&world)->vitals.mp != 40) {
- fprintf(stderr, "render smoke test failed: combat Heal\n");
+ fprintf(stderr, "render smoke test failed: healing enemy target\n");
result = 1;
}
world.entities[world.player_index].vitals.mp =
@@ -339,6 +360,17 @@ main(void)
shop_ui.mode = PH_SHOP_TALK;
context.menu = PH_MENU_SHOP;
ph_present_frame(renderer, &assets, &context);
+ if (ph_render_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1) {
+ fprintf(stderr, "render smoke test failed: return to town\n");
+ result = 1;
+ }
+ context.menu = PH_MENU_NONE;
+ ph_present_frame(renderer, &assets, &context);
+ if (ph_render_enter_portal(&world, PH_AREA_MEADOW) != 1) {
+ fprintf(stderr, "render smoke test failed: return to meadow\n");
+ result = 1;
+ }
+ ph_present_frame(renderer, &assets, &context);
if (!SDL_SaveBMP(surface, PH_RENDER_SMOKE_PATH)) {
fprintf(stderr, "SDL_SaveBMP failed: %s\n", SDL_GetError());
diff --git a/src/tests/smoke.c b/src/tests/smoke.c
@@ -280,7 +280,9 @@ ph_logic_test(int open, int blocked)
world.entities[player_index].vitals.mp = 20;
ph_battle_cast(&battle, &world);
if (battle.phase != PH_BATTLE_TARGET ||
- battle.choice.target_index != enemy_index) return 1;
+ battle.choice.target_index != enemy_index ||
+ ph_battle_target_step(&battle, &world, 1) != player_index ||
+ ph_battle_target_step(&battle, &world, 1) != enemy_index) return 1;
ph_battle_cancel(&battle);
battle.choice.spell = 1;
ph_battle_cast(&battle, &world);
@@ -288,6 +290,7 @@ ph_logic_test(int open, int blocked)
if (battle.phase != PH_BATTLE_TARGET ||
battle.choice.target_index != player_index ||
ally_index < 0 ||
+ ph_battle_target_step(&battle, &world, 1) != enemy_index ||
ph_battle_target_step(&battle, &world, 1) != ally_index ||
ph_battle_target_step(&battle, &world, 1) != player_index) return 1;
ph_battle_cancel(&battle);
@@ -364,6 +367,9 @@ ph_area_shop_test(void)
player->vitals.mp != 40) goto fail;
player->vitals.hp = ph_world_entity_stats(&world, player).max_hp;
player->vitals.mp = ph_world_entity_stats(&world, player).max_mp;
+ if (!ph_area_tile_blocked(&world.area, 26, 1) ||
+ !ph_area_tile_blocked(&world.area, 27, 3) ||
+ ph_area_tile_blocked(&world.area, 28, 3)) goto fail;
if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 ||
world.area_id != PH_AREA_STONEHOLLOW ||
world.initialized_area_count != 2) goto fail;