forked from NotAShelf/rogged
various: add speed/cooldown & initiative system
- Goblins act every ~2 turns, skeletons ~1.5, orcs ~1.2 - Enemies accumulate cooldown based on speed, act when <= 0 - Player always acts every turn (speed = 100)" Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Id822579a0326887d91a80bd4ab27a72d6a6a6964
This commit is contained in:
parent
8f6650919d
commit
db21bdacdd
4 changed files with 185 additions and 41 deletions
11
src/common.h
11
src/common.h
|
|
@ -45,6 +45,12 @@ typedef struct {
|
|||
int defense;
|
||||
int floor;
|
||||
int step_count;
|
||||
int speed; // actions per 100 ticks (100 = 1 action per turn)
|
||||
int cooldown; // countdown to next action (0 = can act)
|
||||
Item equipped_weapon;
|
||||
int has_weapon;
|
||||
Item equipped_armor;
|
||||
int has_armor;
|
||||
Item inventory[MAX_INVENTORY];
|
||||
int inventory_count;
|
||||
} Player;
|
||||
|
|
@ -60,6 +66,8 @@ typedef struct {
|
|||
int attack;
|
||||
int alive;
|
||||
EnemyType type;
|
||||
int speed; // actions per 100 ticks
|
||||
int cooldown; // countdown to next action
|
||||
} Enemy;
|
||||
|
||||
// GameState - encapsulates all game state for testability and save/load
|
||||
|
|
@ -76,6 +84,9 @@ typedef struct {
|
|||
const char *last_message;
|
||||
int message_timer;
|
||||
int turn_count;
|
||||
int awaiting_descend; // 0 = normal, 1 = waiting for Y/N
|
||||
int show_inventory; // 0 = hidden, 1 = show overlay
|
||||
int inv_selected; // currently selected inventory index
|
||||
} GameState;
|
||||
|
||||
#endif // COMMON_H
|
||||
|
|
|
|||
32
src/enemy.c
32
src/enemy.c
|
|
@ -50,23 +50,28 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
|
|||
e.max_hp = ENEMY_BASE_HP + floor;
|
||||
e.hp = e.max_hp;
|
||||
e.attack = ENEMY_BASE_ATTACK;
|
||||
e.speed = 55 + rng_int(0, 10);
|
||||
break;
|
||||
case ENEMY_SKELETON:
|
||||
e.max_hp = ENEMY_BASE_HP + floor + 2;
|
||||
e.hp = e.max_hp;
|
||||
e.attack = ENEMY_BASE_ATTACK + 1;
|
||||
e.speed = 70 + rng_int(0, 10);
|
||||
break;
|
||||
case ENEMY_ORC:
|
||||
e.max_hp = ENEMY_BASE_HP + floor + 4;
|
||||
e.hp = e.max_hp;
|
||||
e.attack = ENEMY_BASE_ATTACK + 2;
|
||||
e.speed = 85 + rng_int(0, 10);
|
||||
break;
|
||||
default:
|
||||
e.max_hp = ENEMY_BASE_HP;
|
||||
e.hp = e.max_hp;
|
||||
e.attack = ENEMY_BASE_ATTACK;
|
||||
e.speed = 60;
|
||||
break;
|
||||
}
|
||||
e.cooldown = e.speed;
|
||||
|
||||
enemies[i] = e;
|
||||
(*count)++;
|
||||
|
|
@ -127,6 +132,21 @@ static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_e
|
|||
}
|
||||
}
|
||||
|
||||
// Perform a single action for an enemy (attack if adjacent, otherwise move)
|
||||
void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
|
||||
if (!e->alive)
|
||||
return;
|
||||
|
||||
// Check if adjacent to player - attack
|
||||
if (can_see_player(e, p)) {
|
||||
combat_enemy_attack(e, p);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, move toward player
|
||||
enemy_move_toward_player(e, p, map, all_enemies, enemy_count);
|
||||
}
|
||||
|
||||
void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
Enemy *e = &enemies[i];
|
||||
|
|
@ -134,14 +154,10 @@ void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {
|
|||
if (!e->alive)
|
||||
continue;
|
||||
|
||||
// Check if adjacent to player - attack
|
||||
if (can_see_player(e, p)) {
|
||||
// Use combat system
|
||||
combat_enemy_attack(e, p);
|
||||
continue;
|
||||
e->cooldown -= e->speed;
|
||||
if (e->cooldown <= 0) {
|
||||
enemy_act(e, p, map, enemies, count);
|
||||
e->cooldown = 100;
|
||||
}
|
||||
|
||||
// Otherwise, move toward player
|
||||
enemy_move_toward_player(e, p, map, enemies, count);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor);
|
|||
// Update all enemy AI
|
||||
void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map);
|
||||
|
||||
// Perform a single action for an enemy (attack if adjacent, otherwise move)
|
||||
void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count);
|
||||
|
||||
// Check if position has an enemy
|
||||
int is_enemy_at(const Enemy *enemies, int count, int x, int y);
|
||||
|
||||
|
|
|
|||
178
src/main.c
178
src/main.c
|
|
@ -60,11 +60,144 @@ static int handle_input(GameState *gs) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Check for item usage (U key)
|
||||
if (gs->show_inventory) {
|
||||
if (IsKeyPressed(KEY_I) || IsKeyPressed(KEY_ESCAPE)) {
|
||||
gs->show_inventory = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_S)) {
|
||||
gs->inv_selected++;
|
||||
if (gs->inv_selected >= gs->player.inventory_count) {
|
||||
gs->inv_selected = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (IsKeyPressed(KEY_UP) || IsKeyPressed(KEY_W)) {
|
||||
if (gs->inv_selected == 0) {
|
||||
gs->inv_selected = (gs->player.inventory_count > 0) ? gs->player.inventory_count - 1 : 0;
|
||||
} else {
|
||||
gs->inv_selected--;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (IsKeyPressed(KEY_ONE))
|
||||
gs->inv_selected = 0;
|
||||
if (IsKeyPressed(KEY_TWO))
|
||||
gs->inv_selected = 1;
|
||||
if (IsKeyPressed(KEY_THREE))
|
||||
gs->inv_selected = 2;
|
||||
if (IsKeyPressed(KEY_FOUR))
|
||||
gs->inv_selected = 3;
|
||||
if (IsKeyPressed(KEY_FIVE))
|
||||
gs->inv_selected = 4;
|
||||
if (IsKeyPressed(KEY_SIX))
|
||||
gs->inv_selected = 5;
|
||||
if (IsKeyPressed(KEY_SEVEN))
|
||||
gs->inv_selected = 6;
|
||||
if (IsKeyPressed(KEY_EIGHT))
|
||||
gs->inv_selected = 7;
|
||||
if (IsKeyPressed(KEY_NINE))
|
||||
gs->inv_selected = 8;
|
||||
if (IsKeyPressed(KEY_ZERO))
|
||||
gs->inv_selected = 9;
|
||||
|
||||
// E to equip selected item
|
||||
if (IsKeyPressed(KEY_E)) {
|
||||
if (player_equip_item(&gs->player, gs->inv_selected)) {
|
||||
gs->last_message = "Item equipped!";
|
||||
gs->message_timer = 60;
|
||||
// Adjust selection if needed
|
||||
if (gs->inv_selected >= gs->player.inventory_count && gs->inv_selected > 0) {
|
||||
gs->inv_selected--;
|
||||
}
|
||||
return 1; // Consume turn
|
||||
} else {
|
||||
gs->last_message = "Cannot equip that!";
|
||||
gs->message_timer = 60;
|
||||
}
|
||||
}
|
||||
|
||||
// U or Enter to use selected item
|
||||
if (IsKeyPressed(KEY_U) || IsKeyPressed(KEY_ENTER)) {
|
||||
if (gs->player.inventory_count > 0) {
|
||||
Item *item = player_get_inventory_item(&gs->player, gs->inv_selected);
|
||||
if (item != NULL) {
|
||||
if (item->type == ITEM_POTION) {
|
||||
player_use_item(&gs->player, item);
|
||||
player_remove_inventory_item(&gs->player, gs->inv_selected);
|
||||
gs->last_message = "Used potion!";
|
||||
gs->message_timer = 60;
|
||||
gs->show_inventory = 0;
|
||||
return 1; // Consume turn
|
||||
} else {
|
||||
gs->last_message = "Equip weapons/armor with E!";
|
||||
gs->message_timer = 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle descend confirmation
|
||||
if (gs->awaiting_descend) {
|
||||
if (IsKeyPressed(KEY_Y)) {
|
||||
// Descend
|
||||
if (gs->player.floor < NUM_FLOORS) {
|
||||
audio_play_stairs();
|
||||
init_floor(gs, gs->player.floor + 1);
|
||||
gs->last_message = "Descended to next floor!";
|
||||
gs->message_timer = 60;
|
||||
gs->awaiting_descend = 0;
|
||||
return 1;
|
||||
} else {
|
||||
gs->game_won = 1;
|
||||
gs->game_over = 1;
|
||||
gs->awaiting_descend = 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (IsKeyPressed(KEY_N)) {
|
||||
gs->awaiting_descend = 0;
|
||||
gs->last_message = "Stayed on floor.";
|
||||
gs->message_timer = 60;
|
||||
return 1;
|
||||
}
|
||||
return 0; // Waiting for Y/N
|
||||
}
|
||||
|
||||
// Check for inventory toggle (I key)
|
||||
if (IsKeyPressed(KEY_I) && !gs->game_over) {
|
||||
gs->show_inventory = 1;
|
||||
gs->inv_selected = 0;
|
||||
return 0; // Don't consume turn
|
||||
}
|
||||
|
||||
// Check for manual item pickup (G key)
|
||||
if (IsKeyPressed(KEY_G) && !gs->game_over) {
|
||||
Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.x, gs->player.y);
|
||||
if (item != NULL) {
|
||||
if (player_pickup(&gs->player, item)) {
|
||||
gs->last_message = "Picked up item!";
|
||||
gs->message_timer = 60;
|
||||
audio_play_item_pickup();
|
||||
return 1; // Consume a turn
|
||||
} else {
|
||||
gs->last_message = "Inventory full!";
|
||||
gs->message_timer = 60;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for item usage (U key - use first potion)
|
||||
if (IsKeyPressed(KEY_U) && !gs->game_over) {
|
||||
if (gs->player.inventory_count > 0) {
|
||||
if (player_use_first_item(&gs->player)) {
|
||||
gs->last_message = "Used item!";
|
||||
gs->last_message = "Used potion!";
|
||||
gs->message_timer = 60;
|
||||
audio_play_item_pickup();
|
||||
return 1; // consume a turn
|
||||
|
|
@ -87,41 +220,19 @@ static int handle_input(GameState *gs) {
|
|||
// Reset combat message
|
||||
combat_reset_event();
|
||||
|
||||
// Check for item at target position before moving
|
||||
int new_x = gs->player.x + dx;
|
||||
int new_y = gs->player.y + dy;
|
||||
int will_pickup = 0;
|
||||
for (int i = 0; i < gs->item_count; i++) {
|
||||
if (!gs->items[i].picked_up && gs->items[i].x == new_x && gs->items[i].y == new_y) {
|
||||
will_pickup = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Player action
|
||||
int action = player_move(&gs->player, dx, dy, &gs->map, gs->enemies, gs->enemy_count, gs->items, gs->item_count);
|
||||
int action = player_move(&gs->player, dx, dy, &gs->map, gs->enemies, gs->enemy_count);
|
||||
|
||||
if (action) {
|
||||
// Play pickup sound if item was picked up
|
||||
if (will_pickup) {
|
||||
audio_play_item_pickup();
|
||||
}
|
||||
// Increment turn counter
|
||||
gs->turn_count++;
|
||||
|
||||
// Check if stepped on stairs
|
||||
if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) {
|
||||
// Go to next floor
|
||||
if (gs->player.floor < NUM_FLOORS) {
|
||||
audio_play_stairs();
|
||||
init_floor(gs, gs->player.floor + 1);
|
||||
gs->last_message = "Descended to next floor!";
|
||||
gs->message_timer = 60;
|
||||
} else {
|
||||
// Won the game
|
||||
gs->game_won = 1;
|
||||
gs->game_over = 1;
|
||||
}
|
||||
gs->awaiting_descend = 1;
|
||||
gs->last_message = "Descend to next floor? (Y/N)";
|
||||
gs->message_timer = 120;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check if killed enemy
|
||||
|
|
@ -134,10 +245,8 @@ static int handle_input(GameState *gs) {
|
|||
}
|
||||
}
|
||||
|
||||
// Enemy turn - only every other turn for fairness
|
||||
if (gs->turn_count % 2 == 0) {
|
||||
// Enemy turns - now uses speed/cooldown system (no more % 2 hack)
|
||||
enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map);
|
||||
}
|
||||
|
||||
// Check if player took damage
|
||||
if (combat_was_player_damage() && combat_get_last_damage() > 0) {
|
||||
|
|
@ -203,6 +312,11 @@ static void game_loop(void) {
|
|||
render_player(&gs.player);
|
||||
render_ui(&gs.player);
|
||||
|
||||
// Draw inventory overlay if active
|
||||
if (gs.show_inventory) {
|
||||
render_inventory_overlay(&gs.player, gs.inv_selected);
|
||||
}
|
||||
|
||||
// Draw message if any
|
||||
if (gs.last_message != NULL && gs.message_timer > 0) {
|
||||
render_message(gs.last_message);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue