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 134 additions and 85 deletions
Showing only changes of commit d01a54161d - Show all commits

refactor(movement): generalize movement and use vectors

Squirrel Modeller 2026-04-09 14:28:41 +02:00
No known key found for this signature in database
GPG key ID: C9FBA7B8C387BF70

View file

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

View file

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

View file

@ -2,6 +2,7 @@
#include "combat.h"
#include "common.h"
#include "map.h"
#include "movement.h"
#include "rng.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);
// Don't spawn on player position
if (ex == p->x && ey == p->y) {
if (ex == p->position.x && ey == p->position.y) {
continue;
}
@ -41,8 +42,8 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
// Create enemy
Enemy e;
memset(&e, 0, sizeof(Enemy));
e.x = ex;
e.y = ey;
e.position.x = ex;
e.position.y = ey;
e.alive = 1;
e.type = rng_int(ENEMY_GOBLIN, max_type);
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
int is_enemy_at(const Enemy *enemies, int count, int x, int y) {
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;
}
}
@ -136,45 +137,37 @@ int is_enemy_at(const Enemy *enemies, int count, int x, int y) {
// Check if enemy can see player (adjacent)
static int can_see_player(Enemy *e, Player *p) {
int dx = p->x - e->x;
int dy = p->y - e->y;
int dx = p->position.x - e->position.x;
int dy = p->position.y - e->position.y;
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
static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
int dx = 0, dy = 0;
if (p->x > e->x)
if (p->position.x > e->position.x)
dx = 1;
else if (p->x < e->x)
else if (p->position.x < e->position.x)
dx = -1;
if (p->y > e->y)
if (p->position.y > e->position.y)
dy = 1;
else if (p->y < e->y)
else if (p->position.y < e->position.y)
dy = -1;
// Try horizontal first, then vertical
int new_x = e->x + dx;
int new_y = e->y;
Vec2 dir = {dx, 0};
if (dx != 0) {
MoveResult r = try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
if (r == MOVE_RESULT_MOVED)
return;
}
if (dx != 0 && 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;
} 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 "items.h"
#include "map.h"
#include "movement.h"
#include "player.h"
#include "raylib.h"
#include "render.h"
@ -117,8 +118,8 @@ static void init_floor(GameState *gs, int floor_num) {
gs->floors_reached = 1;
} else {
// Move player to new floor position
gs->player.x = start_x;
gs->player.y = start_y;
gs->player.position.x = start_x;
gs->player.position.y = start_y;
}
gs->player.floor = floor_num;
@ -137,7 +138,8 @@ static void tick_all_effects(GameState *gs) {
// Player effects
int player_effect_dmg = combat_tick_effects(&gs->player);
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;
}
@ -155,7 +157,7 @@ static void tick_all_effects(GameState *gs) {
continue;
int enemy_effect_dmg = combat_tick_enemy_effects(e);
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) {
add_log(gs, "Enemy died from effects!");
@ -173,7 +175,7 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
return;
// 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->last_message = "Descend to next floor? (Y/N)";
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
if (attacked_enemy != NULL) {
int ex = attacked_enemy->x * TILE_SIZE + 8;
int ey = attacked_enemy->y * TILE_SIZE;
int ex = attacked_enemy->position.x * TILE_SIZE + 8;
int ey = attacked_enemy->position.y * TILE_SIZE;
if (combat_was_dodged()) {
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->damage_taken += combat_get_last_damage();
gs->times_hit++;
spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, combat_get_last_damage(),
combat_was_critical());
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE,
combat_get_last_damage(), combat_was_critical());
}
// Set message and check game over
@ -388,7 +390,7 @@ static int handle_movement_input(GameState *gs) {
// Check for manual item pickup (G key)
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 (player_pickup(&gs->player, item)) {
gs->items_collected++;
@ -417,38 +419,43 @@ 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;
// Reset combat event before player acts
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;
// Attack enemy at target tile, or move into it
Enemy *target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
if (target != NULL) {
MoveResult result =
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);
player_attack(&gs->player, target);
action = 1;
} else {
action = player_move(&gs->player, dx, dy, &gs->map);
}
if (action)
post_action(gs, target); // target is NULL on move, enemy ptr on attack
post_action(gs, target);
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,13 @@
#include "combat.h"
#include "common.h"
#include "items.h"
#include "map.h"
#include "movement.h"
#include "settings.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
void player_init(Player *p, int x, int y) {
p->x = x;
p->y = y;
p->position.x = x;
p->position.y = y;
p->hp = PLAYER_BASE_HP;
p->max_hp = PLAYER_BASE_HP;
p->attack = PLAYER_BASE_ATTACK;
@ -43,29 +41,18 @@ Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y) {
if (count > MAX_ENEMIES)
count = MAX_ENEMIES;
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 NULL;
}
int player_move(Player *p, int dx, int dy, Map *map) {
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))
int player_move(Player *p, Vec2 direction, Map *map, Enemy *enemies, int enemy_count) {
MoveResult result = try_move_entity(&p->position, direction, map, p, enemies, enemy_count, true);
if (result != MOVE_RESULT_MOVED)
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;
// Regen suppressed while poisoned, bleeding, or burning
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_BLEED) &&
@ -75,6 +62,16 @@ int player_move(Player *p, int dx, int dy, Map *map) {
return 1;
}
void player_on_move(Player *p) {
p->step_count += 1;
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_BLEED) &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_BURN)) {
p->hp += 1;
}
}
void player_attack(Player *p, Enemy *e) {
// Use combat system
combat_player_attack(p, e);
@ -228,8 +225,8 @@ int player_drop_item(Player *p, int inv_index, Item *items, int item_count) {
if (items[i].picked_up) {
// Place dropped item at this position
items[i] = *item;
items[i].x = p->x;
items[i].y = p->y;
items[i].x = p->position.x;
items[i].y = p->position.y;
items[i].picked_up = 0;
// Remove from inventory
player_remove_inventory_item(p, inv_index);

View file

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

Typo, should be "healing, etc."

Typo, should be "healing, etc."
void player_on_move(Player *p);
// 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);

View file

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