movement: generalize; use vectors #16

Merged
NotAShelf merged 6 commits from SquirrelModeler/rogged:generalize-movement into main 2026-04-09 14:11:47 +00:00
9 changed files with 124 additions and 89 deletions

View file

@ -25,6 +25,7 @@ pub fn build(b: *std.Build) void {
"src/main.c", "src/main.c",
"src/map.c", "src/map.c",
"src/player.c", "src/player.c",
"src/movement.c",
"src/render.c", "src/render.c",
"src/rng.c", "src/rng.c",
"src/settings.c", "src/settings.c",

View file

@ -4,6 +4,10 @@
#include "settings.h" #include "settings.h"
#include <raylib.h> #include <raylib.h>
typedef struct {
int x, y;
} Vec2;
// Tile types // Tile types
typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType; typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType;
@ -57,7 +61,7 @@ typedef struct {
// Player // Player
typedef struct { typedef struct {
int x, y; Vec2 position;
int hp, max_hp; int hp, max_hp;
int attack; int attack;
int defense; int defense;
@ -83,7 +87,7 @@ typedef enum { ENEMY_GOBLIN, ENEMY_SKELETON, ENEMY_ORC } EnemyType;
// Enemy // Enemy
typedef struct { typedef struct {
int x, y; Vec2 position;
int hp; int hp;
int max_hp; int max_hp;
int attack; int attack;

View file

@ -2,6 +2,7 @@
#include "combat.h" #include "combat.h"
#include "common.h" #include "common.h"
#include "map.h" #include "map.h"
#include "movement.h"
#include "rng.h" #include "rng.h"
#include <string.h> #include <string.h>
@ -29,7 +30,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
get_random_floor_tile(map, &ex, &ey, 50); get_random_floor_tile(map, &ex, &ey, 50);
// Don't spawn on player position // Don't spawn on player position
if (ex == p->x && ey == p->y) { if (ex == p->position.x && ey == p->position.y) {
continue; continue;
} }
@ -41,8 +42,8 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
// Create enemy // Create enemy
Enemy e; Enemy e;
memset(&e, 0, sizeof(Enemy)); memset(&e, 0, sizeof(Enemy));
e.x = ex; e.position.x = ex;
e.y = ey; e.position.y = ey;
e.alive = 1; e.alive = 1;
e.type = rng_int(ENEMY_GOBLIN, max_type); e.type = rng_int(ENEMY_GOBLIN, max_type);
e.effect_count = 0; e.effect_count = 0;
@ -127,7 +128,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
// Check if position has an enemy // Check if position has an enemy
int is_enemy_at(const Enemy *enemies, int count, int x, int y) { int is_enemy_at(const Enemy *enemies, int count, int x, int y) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (enemies[i].alive && enemies[i].x == x && enemies[i].y == y) { if (enemies[i].alive && enemies[i].position.x == x && enemies[i].position.y == y) {
return 1; return 1;
} }
} }
@ -136,45 +137,37 @@ int is_enemy_at(const Enemy *enemies, int count, int x, int y) {
// Check if enemy can see player (adjacent) // Check if enemy can see player (adjacent)
static int can_see_player(Enemy *e, Player *p) { static int can_see_player(Enemy *e, Player *p) {
int dx = p->x - e->x; int dx = p->position.x - e->position.x;
int dy = p->y - e->y; int dy = p->position.y - e->position.y;
return (dx >= -1 && dx <= 1 && dy >= -1 && dy <= 1); return (dx >= -1 && dx <= 1 && dy >= -1 && dy <= 1);
} }
// Check if position is occupied by player
static int is_player_at(Player *p, int x, int y) {
return (p->x == x && p->y == y);
}
// Move enemy toward player // Move enemy toward player
static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) { static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
int dx = 0, dy = 0; int dx = 0, dy = 0;
if (p->x > e->x) if (p->position.x > e->position.x)
dx = 1; dx = 1;
else if (p->x < e->x) else if (p->position.x < e->position.x)
dx = -1; dx = -1;
if (p->y > e->y) if (p->position.y > e->position.y)
dy = 1; dy = 1;
else if (p->y < e->y) else if (p->position.y < e->position.y)
dy = -1; dy = -1;
// Try horizontal first, then vertical Vec2 dir = {dx, 0};
int new_x = e->x + dx; if (dx != 0) {
int new_y = e->y; MoveResult r = try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
if (r == MOVE_RESULT_MOVED)
if (dx != 0 && is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y) && return;
!is_player_at(p, new_x, new_y)) {
e->x = new_x;
} else if (dy != 0) {
new_x = e->x;
new_y = e->y + dy;
if (is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y) &&
!is_player_at(p, new_x, new_y)) {
e->x = new_x;
e->y = new_y;
} }
dir.x = 0;
dir.y = dy;
if (dy != 0) {
try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
} }
} }

View file

@ -4,6 +4,7 @@
#include "enemy.h" #include "enemy.h"
#include "items.h" #include "items.h"
#include "map.h" #include "map.h"
#include "movement.h"
#include "player.h" #include "player.h"
#include "raylib.h" #include "raylib.h"
#include "render.h" #include "render.h"
@ -117,8 +118,8 @@ static void init_floor(GameState *gs, int floor_num) {
gs->floors_reached = 1; gs->floors_reached = 1;
} else { } else {
// Move player to new floor position // Move player to new floor position
gs->player.x = start_x; gs->player.position.x = start_x;
gs->player.y = start_y; gs->player.position.y = start_y;
} }
gs->player.floor = floor_num; gs->player.floor = floor_num;
@ -137,7 +138,8 @@ static void tick_all_effects(GameState *gs) {
// Player effects // Player effects
int player_effect_dmg = combat_tick_effects(&gs->player); int player_effect_dmg = combat_tick_effects(&gs->player);
if (player_effect_dmg > 0) { if (player_effect_dmg > 0) {
spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, player_effect_dmg, 0); spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE, player_effect_dmg,
0);
gs->screen_shake = SHAKE_EFFECT_DURATION; gs->screen_shake = SHAKE_EFFECT_DURATION;
} }
@ -155,7 +157,7 @@ static void tick_all_effects(GameState *gs) {
continue; continue;
int enemy_effect_dmg = combat_tick_enemy_effects(e); int enemy_effect_dmg = combat_tick_enemy_effects(e);
if (enemy_effect_dmg > 0) { if (enemy_effect_dmg > 0) {
spawn_floating_text(gs, e->x * TILE_SIZE + 8, e->y * TILE_SIZE, enemy_effect_dmg, 0); spawn_floating_text(gs, e->position.x * TILE_SIZE + 8, e->position.y * TILE_SIZE, enemy_effect_dmg, 0);
} }
if (!e->alive) { if (!e->alive) {
add_log(gs, "Enemy died from effects!"); add_log(gs, "Enemy died from effects!");
@ -173,7 +175,7 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
return; return;
// Check if stepped on stairs // Check if stepped on stairs
if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) { if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) {
gs->awaiting_descend = 1; gs->awaiting_descend = 1;
gs->last_message = "Descend to next floor? (Y/N)"; gs->last_message = "Descend to next floor? (Y/N)";
gs->message_timer = 120; gs->message_timer = 120;
@ -182,8 +184,8 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
// combat feedback - player attacked an enemy this turn // combat feedback - player attacked an enemy this turn
if (attacked_enemy != NULL) { if (attacked_enemy != NULL) {
int ex = attacked_enemy->x * TILE_SIZE + 8; int ex = attacked_enemy->position.x * TILE_SIZE + 8;
int ey = attacked_enemy->y * TILE_SIZE; int ey = attacked_enemy->position.y * TILE_SIZE;
if (combat_was_dodged()) { if (combat_was_dodged()) {
spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE); spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE);
@ -224,8 +226,8 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION; gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION;
gs->damage_taken += combat_get_last_damage(); gs->damage_taken += combat_get_last_damage();
gs->times_hit++; gs->times_hit++;
spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, combat_get_last_damage(), spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE,
combat_was_critical()); combat_get_last_damage(), combat_was_critical());
} }
// Set message and check game over // Set message and check game over
@ -388,7 +390,7 @@ static int handle_movement_input(GameState *gs) {
// Check for manual item pickup (G key) // Check for manual item pickup (G key)
if (IsKeyPressed(KEY_G)) { if (IsKeyPressed(KEY_G)) {
Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.x, gs->player.y); Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.position.x, gs->player.position.y);
if (item != NULL) { if (item != NULL) {
if (player_pickup(&gs->player, item)) { if (player_pickup(&gs->player, item)) {
gs->items_collected++; gs->items_collected++;
@ -417,38 +419,45 @@ static int handle_movement_input(GameState *gs) {
} }
} }
// Movement: use IsKeyDown for held-key repeat
int dx = 0, dy = 0;
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))
dy = -1;
else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))
dy = 1;
else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))
dx = -1;
else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT))
dx = 1;
if (dx == 0 && dy == 0) Vec2 direction = {0, 0};
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))
direction.y = -1;
else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))
direction.y = 1;
else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))
direction.x = -1;
else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT))
direction.x = 1;
if (direction.x == 0 && direction.y == 0)
return 0; return 0;
// Reset combat event before player acts // Reset combat event before player acts
combat_reset_event(); combat_reset_event();
int new_x = gs->player.x + dx;
int new_y = gs->player.y + dy; int new_x = gs->player.position.x + direction.x;
int new_y = gs->player.position.y + direction.y;
Enemy *target = NULL;
int action = 0; int action = 0;
// Attack enemy at target tile, or move into it MoveResult result =
Enemy *target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y); try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true);
if (result == MOVE_RESULT_MOVED) {
player_on_move(&gs->player);
action = 1;
} else if (result == MOVE_RESULT_BLOCKED_ENEMY) {
SquirrelModeler marked this conversation as resolved Outdated

You should probably guard against potential null target here before attacking.

Because if try_move_entity returns MOVE_RESULT_BLOCKED_ENEMY but player_find_enemy_at fails to find the enemy (be it due to differing lookup logic, or if the enemy was killed by effects earlier in the turn), target would be NULL, and thus lead to an ub in player_attack. Something like:

   } else if (result == MOVE_RESULT_BLOCKED_ENEMY) {
     target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
-    player_attack(&gs->player, target);
-    action = 1;
+    if (target != NULL) {
+      player_attack(&gs->player, target);
+      action = 1;
+    }
   }
You should probably guard against potential null target here before attacking. Because if `try_move_entity` returns `MOVE_RESULT_BLOCKED_ENEMY` but `player_find_enemy_at` fails to find the enemy (be it due to differing lookup logic, or if the enemy was killed by effects earlier in the turn), target would be `NULL`, and thus lead to an ub in `player_attack`. Something like: ```diff } else if (result == MOVE_RESULT_BLOCKED_ENEMY) { target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y); - player_attack(&gs->player, target); - action = 1; + if (target != NULL) { + player_attack(&gs->player, target); + action = 1; + } } ```

If we move the action = 1 into the null guarded if statement, then enemy AI breaks. We can do:

if (target != NULL) {
  player_attack(&gs->player, target);
}
action = 1;

Which fixes that bug.

Ignore this. I added an incorrect guard.

~~If we move the action = 1 into the null guarded if statement, then enemy AI breaks. We can do:~~ ``` if (target != NULL) { player_attack(&gs->player, target); } action = 1; ``` ~~Which fixes that bug.~~ Ignore this. I added an incorrect guard.
target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
if (target != NULL) { if (target != NULL) {
player_attack(&gs->player, target); player_attack(&gs->player, target);
action = 1; action = 1;
} else { }
action = player_move(&gs->player, dx, dy, &gs->map);
} }
if (action) if (action)
post_action(gs, target); // target is NULL on move, enemy ptr on attack post_action(gs, target);
return action; return action;
} }

29
src/movement.c Normal file
View file

@ -0,0 +1,29 @@
#include "movement.h"
#include "enemy.h"
#include "map.h"
#include <stdbool.h>
// Check if position is occupied by player
static int is_player_at(Player *p, int x, int y) {
return (p->position.x == x && p->position.y == y);
}
MoveResult try_move_entity(Vec2 *p, Vec2 direction, Map *map, Player *player, Enemy *enemies, int enemy_count,
bool moving_is_player) {
int new_x = p->x + direction.x;
int new_y = p->y + direction.y;
if (!is_floor(map, new_x, new_y))
return MOVE_RESULT_BLOCKED_WALL;
if (is_enemy_at(enemies, enemy_count, new_x, new_y))
return MOVE_RESULT_BLOCKED_ENEMY;
if (!moving_is_player) {
if (is_player_at(player, new_x, new_y))
return MOVE_RESULT_BLOCKED_PLAYER;
}
p->x = new_x;
p->y = new_y;
return MOVE_RESULT_MOVED;
}

17
src/movement.h Normal file
View file

@ -0,0 +1,17 @@
#ifndef MOVEMENT_H
#define MOVEMENT_H
#include "common.h"
typedef enum {
MOVE_RESULT_MOVED,
MOVE_RESULT_BLOCKED_WALL,
MOVE_RESULT_BLOCKED_PLAYER,
MOVE_RESULT_BLOCKED_ENEMY
} MoveResult;
// Attempts to move entity in a given direction. Returns outcome of action.
MoveResult try_move_entity(Vec2 *p, Vec2 direction, Map *map, Player *player, Enemy *enemies, int enemy_count,
bool moving_is_player);
#endif // MOVEMENT_H

View file

@ -2,15 +2,12 @@
#include "combat.h" #include "combat.h"
#include "common.h" #include "common.h"
#include "items.h" #include "items.h"
#include "map.h"
#include "settings.h" #include "settings.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
void player_init(Player *p, int x, int y) { void player_init(Player *p, int x, int y) {
p->x = x; p->position.x = x;
p->y = y; p->position.y = y;
p->hp = PLAYER_BASE_HP; p->hp = PLAYER_BASE_HP;
p->max_hp = PLAYER_BASE_HP; p->max_hp = PLAYER_BASE_HP;
p->attack = PLAYER_BASE_ATTACK; p->attack = PLAYER_BASE_ATTACK;
@ -43,36 +40,20 @@ Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y) {
if (count > MAX_ENEMIES) if (count > MAX_ENEMIES)
count = MAX_ENEMIES; count = MAX_ENEMIES;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (enemies[i].alive && enemies[i].x == x && enemies[i].y == y) if (enemies[i].alive && enemies[i].position.x == x && enemies[i].position.y == y)
return &enemies[i]; return &enemies[i];
} }
return NULL; return NULL;
} }
int player_move(Player *p, int dx, int dy, Map *map) { void player_on_move(Player *p) {
int new_x = p->x + dx;
int new_y = p->y + dy;
// Check bounds
if (!in_bounds(new_x, new_y, MAP_WIDTH, MAP_HEIGHT))
return 0;
// Check if walkable
if (!is_floor(map, new_x, new_y))
return 0;
// Move player
p->x = new_x;
p->y = new_y;
p->step_count += 1; p->step_count += 1;
// Regen suppressed while poisoned, bleeding, or burning
if (p->step_count % REGEN_STEP_INTERVAL == 0 && p->hp < p->max_hp && if (p->step_count % REGEN_STEP_INTERVAL == 0 && p->hp < p->max_hp &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_POISON) && !combat_has_effect(p->effects, p->effect_count, EFFECT_POISON) &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_BLEED) && !combat_has_effect(p->effects, p->effect_count, EFFECT_BLEED) &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_BURN)) { !combat_has_effect(p->effects, p->effect_count, EFFECT_BURN)) {
p->hp += 1; p->hp += 1;
} }
return 1;
} }
void player_attack(Player *p, Enemy *e) { void player_attack(Player *p, Enemy *e) {
@ -228,8 +209,8 @@ int player_drop_item(Player *p, int inv_index, Item *items, int item_count) {
if (items[i].picked_up) { if (items[i].picked_up) {
// Place dropped item at this position // Place dropped item at this position
items[i] = *item; items[i] = *item;
items[i].x = p->x; items[i].x = p->position.x;
items[i].y = p->y; items[i].y = p->position.y;
items[i].picked_up = 0; items[i].picked_up = 0;
// Remove from inventory // Remove from inventory
player_remove_inventory_item(p, inv_index); player_remove_inventory_item(p, inv_index);

View file

@ -6,8 +6,8 @@
// Initialize player at position // Initialize player at position
void player_init(Player *p, int x, int y); void player_init(Player *p, int x, int y);
// Move player to (x+dx, y+dy); returns 1 if moved, 0 if blocked // Apply status effects, healing, etc
SquirrelModeler marked this conversation as resolved Outdated

Typo, should be "healing, etc."

Typo, should be "healing, etc."
int player_move(Player *p, int dx, int dy, Map *map); void player_on_move(Player *p);
// Find a living enemy at tile (x, y); returns NULL if none // Find a living enemy at tile (x, y); returns NULL if none
Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y); Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y);

View file

@ -30,7 +30,8 @@ void render_map(const Map *map) {
} }
void render_player(const Player *p) { void render_player(const Player *p) {
Rectangle rect = {(float)(p->x * TILE_SIZE), (float)(p->y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE}; Rectangle rect = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE,
(float)TILE_SIZE};
DrawRectangleRec(rect, BLUE); DrawRectangleRec(rect, BLUE);
} }
@ -39,8 +40,8 @@ void render_enemies(const Enemy *enemies, int count) {
if (!enemies[i].alive) if (!enemies[i].alive)
continue; continue;
Rectangle rect = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE), (float)TILE_SIZE, Rectangle rect = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE),
(float)TILE_SIZE}; (float)TILE_SIZE, (float)TILE_SIZE};
// Different colors based on enemy type // Different colors based on enemy type
Color enemy_color; Color enemy_color;
@ -72,8 +73,8 @@ void render_enemies(const Enemy *enemies, int count) {
bar_color = (Color){200, 180, 40, 255}; // yellow bar_color = (Color){200, 180, 40, 255}; // yellow
else else
bar_color = (Color){200, 60, 60, 255}; // red bar_color = (Color){200, 60, 60, 255}; // red
Rectangle hp_bar = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE - 4), (float)hp_pixels, Rectangle hp_bar = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE - 4),
3}; (float)hp_pixels, 3};
DrawRectangleRec(hp_bar, bar_color); DrawRectangleRec(hp_bar, bar_color);
} }
} }