From db21bdacddfbce5dbd892678fc2ae5ce4eca29c8 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 3 Apr 2026 14:21:05 +0300 Subject: [PATCH] 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 Change-Id: Id822579a0326887d91a80bd4ab27a72d6a6a6964 --- src/common.h | 11 ++++ src/enemy.c | 32 ++++++--- src/enemy.h | 3 + src/main.c | 180 +++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 185 insertions(+), 41 deletions(-) diff --git a/src/common.h b/src/common.h index e0df30c..30b1c3c 100644 --- a/src/common.h +++ b/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 diff --git a/src/enemy.c b/src/enemy.c index d864d8d..f6ca10f 100644 --- a/src/enemy.c +++ b/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); } } diff --git a/src/enemy.h b/src/enemy.h index d7b2e6c..8eaf852 100644 --- a/src/enemy.h +++ b/src/enemy.h @@ -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); diff --git a/src/main.c b/src/main.c index 9430bfd..44e66ba 100644 --- a/src/main.c +++ b/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_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map); - } + // 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);