movement: generalize; use vectors #16
9 changed files with 134 additions and 85 deletions
refactor(movement): generalize movement and use vectors
commit
d01a54161d
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
49
src/enemy.c
49
src/enemy.c
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
65
src/main.c
65
src/main.c
|
|
@ -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
|
||||
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
29
src/movement.c
Normal 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
17
src/movement.h
Normal 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
|
||||
41
src/player.c
41
src/player.c
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
NotAShelf
commented
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);
|
||||
|
|
|
|||
11
src/render.c
11
src/render.c
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue
You should probably guard against potential null target here before attacking.
Because if
try_move_entityreturnsMOVE_RESULT_BLOCKED_ENEMYbutplayer_find_enemy_atfails 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 beNULL, and thus lead to an ub inplayer_attack. Something like:If we move the action = 1 into the null guarded if statement, then enemy AI breaks. We can do:Which fixes that bug.Ignore this. I added an incorrect guard.