1
0
Fork 0
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:
raf 2026-04-03 14:21:05 +03:00
commit db21bdacdd
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 185 additions and 41 deletions

View file

@ -45,6 +45,12 @@ typedef struct {
int defense; int defense;
int floor; int floor;
int step_count; 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]; Item inventory[MAX_INVENTORY];
int inventory_count; int inventory_count;
} Player; } Player;
@ -60,6 +66,8 @@ typedef struct {
int attack; int attack;
int alive; int alive;
EnemyType type; EnemyType type;
int speed; // actions per 100 ticks
int cooldown; // countdown to next action
} Enemy; } Enemy;
// GameState - encapsulates all game state for testability and save/load // GameState - encapsulates all game state for testability and save/load
@ -76,6 +84,9 @@ typedef struct {
const char *last_message; const char *last_message;
int message_timer; int message_timer;
int turn_count; 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; } GameState;
#endif // COMMON_H #endif // COMMON_H

View file

@ -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.max_hp = ENEMY_BASE_HP + floor;
e.hp = e.max_hp; e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK; e.attack = ENEMY_BASE_ATTACK;
e.speed = 55 + rng_int(0, 10);
break; break;
case ENEMY_SKELETON: case ENEMY_SKELETON:
e.max_hp = ENEMY_BASE_HP + floor + 2; e.max_hp = ENEMY_BASE_HP + floor + 2;
e.hp = e.max_hp; e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK + 1; e.attack = ENEMY_BASE_ATTACK + 1;
e.speed = 70 + rng_int(0, 10);
break; break;
case ENEMY_ORC: case ENEMY_ORC:
e.max_hp = ENEMY_BASE_HP + floor + 4; e.max_hp = ENEMY_BASE_HP + floor + 4;
e.hp = e.max_hp; e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK + 2; e.attack = ENEMY_BASE_ATTACK + 2;
e.speed = 85 + rng_int(0, 10);
break; break;
default: default:
e.max_hp = ENEMY_BASE_HP; e.max_hp = ENEMY_BASE_HP;
e.hp = e.max_hp; e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK; e.attack = ENEMY_BASE_ATTACK;
e.speed = 60;
break; break;
} }
e.cooldown = e.speed;
enemies[i] = e; enemies[i] = e;
(*count)++; (*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) { void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Enemy *e = &enemies[i]; Enemy *e = &enemies[i];
@ -134,14 +154,10 @@ void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {
if (!e->alive) if (!e->alive)
continue; continue;
// Check if adjacent to player - attack e->cooldown -= e->speed;
if (can_see_player(e, p)) { if (e->cooldown <= 0) {
// Use combat system enemy_act(e, p, map, enemies, count);
combat_enemy_attack(e, p); e->cooldown = 100;
continue;
} }
// Otherwise, move toward player
enemy_move_toward_player(e, p, map, enemies, count);
} }
} }

View file

@ -9,6 +9,9 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor);
// Update all enemy AI // Update all enemy AI
void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map); 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 // 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);

View file

@ -60,11 +60,144 @@ static int handle_input(GameState *gs) {
return 0; 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 (IsKeyPressed(KEY_U) && !gs->game_over) {
if (gs->player.inventory_count > 0) { if (gs->player.inventory_count > 0) {
if (player_use_first_item(&gs->player)) { if (player_use_first_item(&gs->player)) {
gs->last_message = "Used item!"; gs->last_message = "Used potion!";
gs->message_timer = 60; gs->message_timer = 60;
audio_play_item_pickup(); audio_play_item_pickup();
return 1; // consume a turn return 1; // consume a turn
@ -87,41 +220,19 @@ static int handle_input(GameState *gs) {
// Reset combat message // Reset combat message
combat_reset_event(); 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 // 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) { if (action) {
// Play pickup sound if item was picked up
if (will_pickup) {
audio_play_item_pickup();
}
// Increment turn counter // Increment turn counter
gs->turn_count++; gs->turn_count++;
// 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.y][gs->player.x] == TILE_STAIRS) {
// Go to next floor gs->awaiting_descend = 1;
if (gs->player.floor < NUM_FLOORS) { gs->last_message = "Descend to next floor? (Y/N)";
audio_play_stairs(); gs->message_timer = 120;
init_floor(gs, gs->player.floor + 1); return 1;
gs->last_message = "Descended to next floor!";
gs->message_timer = 60;
} else {
// Won the game
gs->game_won = 1;
gs->game_over = 1;
}
} }
// Check if killed enemy // Check if killed enemy
@ -134,10 +245,8 @@ static int handle_input(GameState *gs) {
} }
} }
// Enemy turn - only every other turn for fairness // Enemy turns - now uses speed/cooldown system (no more % 2 hack)
if (gs->turn_count % 2 == 0) { enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map);
enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map);
}
// Check if player took damage // Check if player took damage
if (combat_was_player_damage() && combat_get_last_damage() > 0) { if (combat_was_player_damage() && combat_get_last_damage() > 0) {
@ -203,6 +312,11 @@ static void game_loop(void) {
render_player(&gs.player); render_player(&gs.player);
render_ui(&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 // Draw message if any
if (gs.last_message != NULL && gs.message_timer > 0) { if (gs.last_message != NULL && gs.message_timer > 0) {
render_message(gs.last_message); render_message(gs.last_message);