From df80fa89953ddd007930a2ae5a08ebd02578b416 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 19 May 2026 13:48:16 -0700 Subject: [PATCH] Add superpowers system: 9 abilities with cooldowns, research unlocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New superpowers module with 9 powers (Evil Laugh, Death Ray, Dark Regeneration, Emergency Reinforcements, Force Wave, Soul Drain, Impenetrable Shield, Blink, Plague Cloud) - Powers unlocked via research tree + notoriety thresholds - Combat integration: activate_power() handles all effects - Invulnerability blocks throne attacks, repels agents - Terminal: number keys 1-9 during defend, sidebar display - Bevy: number keys 1-9, UI text display, cooldown ticking - Renamed all Overlord/Evil Genius references to Supervillain - 98 tests passing (4 new superpower combat tests) 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- src/bin/bevy.rs | 313 +++++++++++++++++++++++--------------- src/bin/terminal/input.rs | 19 +++ src/bin/terminal/mod.rs | 70 ++++++++- src/bin/terminal/ui.rs | 22 ++- src/combat.rs | 303 +++++++++++++++++++++++++++++++++++- src/entities/mod.rs | 2 +- src/entities/player.rs | 32 +++- src/lib.rs | 1 + src/main.rs | 6 +- src/superpowers.rs | 260 +++++++++++++++++++++++++++++++ src/tiles.rs | 10 ++ src/ui.rs | 6 +- 12 files changed, 906 insertions(+), 138 deletions(-) create mode 100644 src/superpowers.rs diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 20e287d..a5557e7 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -1,6 +1,6 @@ //! Supervillain — Bevy frontend //! -//! A graphical roguelike inspired by Evil Genius, rendered with Bevy. +//! A graphical roguelike inspired by Supervillain, rendered with Bevy. use bevy::prelude::*; use bevy::window::WindowResolution; @@ -50,9 +50,7 @@ fn tile_color(tile: tiles::TileType) -> Color { /// Brighten a color by a small amount for the "powered glow" effect. fn brighten(color: Color, amount: f32) -> Color { - let Color::Srgba(srgba) = color.to_srgba() else { - return color; - }; + let srgba = color.to_srgba(); Color::srgba( (srgba.red + amount).min(1.0), (srgba.green + amount).min(1.0), @@ -120,7 +118,11 @@ impl GameState { let throne = game_map.throne_pos().unwrap_or((40, 20)); Self { game_map, - player: entities::Player::new(throne.0, throne.1), + player: { + let mut p = entities::Player::new(throne.0, throne.1); + p.update_powers(); + p + }, minions: Vec::new(), agents: Vec::new(), henchmen: entities::Henchman::starting_roster(throne.0, throne.1), @@ -157,7 +159,7 @@ fn main() { App::new() .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { - title: "Supervillain — Evil Genius Roguelike".to_string(), + title: "Supervillain — Supervillain Roguelike".to_string(), resolution: WindowResolution::new(1280, 720), ..default() }), @@ -305,37 +307,38 @@ fn handle_input( let unlocks = tiles::research_unlocks(); if game_state.research_selected < unlocks.len() { let (key, cost, name) = unlocks[game_state.research_selected]; - let gs = &mut **game_state; - if gs.player.unlocked.contains(key) { - gs.log.push("Already researched!".to_string()); - } else if gs.player.research < cost { - gs.log.push(format!( + let already_unlocked = game_state.player.unlocked.contains(key); + let current_research = game_state.player.research; + if already_unlocked { + game_state.log.push("Already researched!".to_string()); + } else if current_research < cost { + game_state.log.push(format!( "Not enough research points! Need {}, have {}", - cost, gs.player.research + cost, current_research )); } else { - gs.player.research -= cost; - gs.player.unlocked.insert(key.to_string()); + game_state.player.research -= cost; + game_state.player.unlocked.insert(key.to_string()); // Apply special effects match key { - "power_core_mk2" => gs.player.power_core_mk2 = true, + "power_core_mk2" => game_state.player.power_core_mk2 = true, "minion_armor" => { - for minion in &mut gs.minions { + for minion in &mut game_state.minions { minion.armor += 5; minion.entity.max_hp += 15; minion.entity.hp = minion.entity.max_hp; } - gs.player.minion_hp_bonus += 15; + game_state.player.minion_hp_bonus += 15; } "minion_weapons" => { - for minion in &mut gs.minions { + for minion in &mut game_state.minions { minion.entity.damage += 5; } - gs.player.minion_damage_bonus += 5; + game_state.player.minion_damage_bonus += 5; } _ => {} } - gs.log.push(format!("Researched: {name}!")); + game_state.log.push(format!("Researched: {name}!")); } } } @@ -367,38 +370,36 @@ fn handle_input( let classes = entities::MinionClass::all(); if game_state.hire_selected < classes.len() { let class = classes[game_state.hire_selected]; - let gs = &mut **game_state; let unlock_key = class.unlock_key(); - if !gs.player.unlocked.contains(unlock_key) { - gs.log.push(format!("{} not researched yet!", class.name())); + if !game_state.player.unlocked.contains(unlock_key) { + game_state.log.push(format!("{} not researched yet!", class.name())); } else { let cost = class.hire_cost(); - if gs.player.gold < cost { - gs.log.push(format!("Need {} gold to hire {}!", cost, class.name())); + if game_state.player.gold < cost { + game_state.log.push(format!("Need {} gold to hire {}!", cost, class.name())); } else { - let alive_minions = gs.minions.iter().filter(|m| m.entity.alive).count() as i32; - if alive_minions >= gs.player.minion_cap { - gs.log.push(format!( + let alive_minions = game_state.minions.iter().filter(|m| m.entity.alive).count() as i32; + let minion_cap = game_state.player.minion_cap; + if alive_minions >= minion_cap { + game_state.log.push(format!( "Minion cap reached! ({}/{})", - alive_minions, gs.player.minion_cap + alive_minions, minion_cap )); } else { // Place near throne - let throne = gs.game_map.throne_pos().unwrap_or((40, 20)); + let throne = game_state.game_map.throne_pos().unwrap_or((40, 20)); let mut placed = false; for dist in 1..5 { for (dx, dy) in [(0, -dist), (0, dist), (-dist, 0), (dist, 0)] { let mx = throne.0 + dx; let my = throne.1 + dy; - if gs.game_map.is_walkable(mx, my) { - gs.player.gold -= cost; - let minion = entities::Minion::new( - mx, my, class, - gs.player.minion_damage_bonus, - gs.player.minion_hp_bonus, - ); - gs.minions.push(minion); - gs.log.push(format!("Hired {} for {} gold!", class.name(), cost)); + if game_state.game_map.is_walkable(mx, my) { + game_state.player.gold -= cost; + let dmg_bonus = game_state.player.minion_damage_bonus; + let hp_bonus = game_state.player.minion_hp_bonus; + let minion = entities::Minion::new(mx, my, class, dmg_bonus, hp_bonus); + game_state.minions.push(minion); + game_state.log.push(format!("Hired {} for {} gold!", class.name(), cost)); placed = true; break; } @@ -406,7 +407,7 @@ fn handle_input( if placed { break; } } if !placed { - gs.log.push("No room near throne to place minion!".to_string()); + game_state.log.push("No room near throne to place minion!".to_string()); } } } @@ -442,15 +443,14 @@ fn handle_input( let schemes = tiles::available_schemes(); if idx < schemes.len() { let scheme = &schemes[idx]; - let gs = &mut **game_state; - let alive_minions = gs.minions.iter().filter(|m| m.entity.alive).count() as i32; + let alive_minions = game_state.minions.iter().filter(|m| m.entity.alive).count() as i32; if alive_minions < scheme.minion_cost { - gs.log.push(format!("Need {} minions for this scheme!", scheme.minion_cost)); + game_state.log.push(format!("Need {} minions for this scheme!", scheme.minion_cost)); } else { // Remove minions (send them on the mission) let mut removed = 0i32; let cost = scheme.minion_cost; - gs.minions.retain(|m| { + game_state.minions.retain(|m| { if removed >= cost { return true; } if m.entity.alive && m.class == entities::MinionClass::Worker { removed += 1; @@ -460,7 +460,7 @@ fn handle_input( } }); if removed < cost { - gs.minions.retain(|m| { + game_state.minions.retain(|m| { if removed >= cost { return true; } if m.entity.alive { removed += 1; @@ -471,7 +471,7 @@ fn handle_input( }); } - gs.player.active_schemes.push(entities::ActiveScheme { + game_state.player.active_schemes.push(entities::ActiveScheme { name: scheme.name.to_string(), turns_remaining: scheme.duration_turns, gold_reward: scheme.gold_reward, @@ -480,12 +480,12 @@ fn handle_input( minion_cost: scheme.minion_cost, loot: scheme.loot.map(|s| s.to_string()), }); - gs.log.push(format!("Launched scheme: {}!", scheme.name)); - gs.log.push(format!( + game_state.log.push(format!("Launched scheme: {}!", scheme.name)); + game_state.log.push(format!( " {} minions deployed, {} turns", scheme.minion_cost, scheme.duration_turns )); - gs.phase = Phase::Build; + game_state.phase = Phase::Build; } } } @@ -504,108 +504,121 @@ fn handle_input( if game_state.build_mode.is_some() { let speed = if keyboard.pressed(KeyCode::ShiftLeft) { 2 } else { 1 }; - // Cursor movement in build mode - if keyboard.just_pressed(KeyCode::KeyH) || keyboard.just_pressed(KeyCode::ArrowLeft) { - if let Some(ref mut bm) = game_state.build_mode { - bm.move_cursor(-speed, 0, &game_state.game_map); - } - } - if keyboard.just_pressed(KeyCode::KeyL) || keyboard.just_pressed(KeyCode::ArrowRight) { - if let Some(ref mut bm) = game_state.build_mode { - bm.move_cursor(speed, 0, &game_state.game_map); - } - } - if keyboard.just_pressed(KeyCode::KeyK) || keyboard.just_pressed(KeyCode::ArrowUp) { - if let Some(ref mut bm) = game_state.build_mode { - bm.move_cursor(0, -speed, &game_state.game_map); - } - } - if keyboard.just_pressed(KeyCode::KeyJ) || keyboard.just_pressed(KeyCode::ArrowDown) { - if let Some(ref mut bm) = game_state.build_mode { - bm.move_cursor(0, speed, &game_state.game_map); - } + // Cursor movement in build mode — take build_mode out, move, put back + if keyboard.just_pressed(KeyCode::KeyH) + || keyboard.just_pressed(KeyCode::ArrowLeft) + || keyboard.just_pressed(KeyCode::KeyL) + || keyboard.just_pressed(KeyCode::ArrowRight) + || keyboard.just_pressed(KeyCode::KeyK) + || keyboard.just_pressed(KeyCode::ArrowUp) + || keyboard.just_pressed(KeyCode::KeyJ) + || keyboard.just_pressed(KeyCode::ArrowDown) + { + let mut bm = game_state.build_mode.take().unwrap(); + let (dx, dy) = if keyboard.just_pressed(KeyCode::KeyH) || keyboard.just_pressed(KeyCode::ArrowLeft) { + (-speed, 0) + } else if keyboard.just_pressed(KeyCode::KeyL) || keyboard.just_pressed(KeyCode::ArrowRight) { + (speed, 0) + } else if keyboard.just_pressed(KeyCode::KeyK) || keyboard.just_pressed(KeyCode::ArrowUp) { + (0, -speed) + } else { + (0, speed) + }; + bm.move_cursor(dx, dy, &game_state.game_map); + game_state.build_mode = Some(bm); } - // TAB: cycle category + // TAB: cycle category — take build_mode out, operate, put back if keyboard.just_pressed(KeyCode::Tab) { - if let Some(ref mut bm) = game_state.build_mode { - bm.select_category(1, &game_state.player); - } + let mut bm = game_state.build_mode.take().unwrap(); + let player = game_state.player.clone(); + bm.select_category(1, &player); + game_state.build_mode = Some(bm); } // N: cycle item forward if keyboard.just_pressed(KeyCode::KeyN) { - if let Some(ref mut bm) = game_state.build_mode { - bm.select_item(1, &game_state.player); - } + let mut bm = game_state.build_mode.take().unwrap(); + let player = game_state.player.clone(); + bm.select_item(1, &player); + game_state.build_mode = Some(bm); } // P: cycle item backward if keyboard.just_pressed(KeyCode::KeyP) { - if let Some(ref mut bm) = game_state.build_mode { - bm.select_item(-1, &game_state.player); - } + let mut bm = game_state.build_mode.take().unwrap(); + let player = game_state.player.clone(); + bm.select_item(-1, &player); + game_state.build_mode = Some(bm); } - // SPACE: place tile + // SPACE: place tile — destructure game_state to split borrows if keyboard.just_pressed(KeyCode::Space) { - let result = { - let gs = &mut **game_state; - if let Some(ref mut bm) = gs.build_mode { - let result = bm.place_tile(&mut gs.game_map, &mut gs.player); - let msg = bm.message.clone(); - let cx = bm.cursor_x; - let cy = bm.cursor_y; - (result, msg, cx, cy) - } else { - return; - } - }; - - match result.0 { + let GameState { + build_mode, + game_map, + player, + minions, + log, + .. + } = &mut *game_state; + + let mut bm = build_mode.take().unwrap(); + let result = bm.place_tile(game_map, player); + let msg = bm.message.clone(); + let cx = bm.cursor_x; + let cy = bm.cursor_y; + *build_mode = Some(bm); + + match result { build::BuildResult::GuardPostPlaced => { - game_state.log.push(result.1.clone()); + log.push(msg); // Spawn guard minion at guard post - let gs = &mut **game_state; - let alive_minions = gs.minions.iter().filter(|m| m.entity.alive).count() as i32; - if alive_minions < gs.player.minion_cap { - let minion = entities::Minion::new( - result.2, result.3, - entities::MinionClass::Guard, - gs.player.minion_damage_bonus, - gs.player.minion_hp_bonus, - ); - gs.minions.push(minion); - gs.log.push("Minion deployed at guard post!".to_string()); + let alive_minions = minions.iter().filter(|m| m.entity.alive).count() as i32; + let minion_cap = player.minion_cap; + if alive_minions < minion_cap { + let dmg_bonus = player.minion_damage_bonus; + let hp_bonus = player.minion_hp_bonus; + let minion = entities::Minion::new(cx, cy, entities::MinionClass::Guard, dmg_bonus, hp_bonus); + minions.push(minion); + log.push("Minion deployed at guard post!".to_string()); } else { - gs.log.push(format!( + log.push(format!( "Minion cap reached! ({}/{})", - alive_minions, gs.player.minion_cap + alive_minions, minion_cap )); } } build::BuildResult::Success => { - game_state.log.push(result.1.clone()); + log.push(msg); } build::BuildResult::None => { - game_state.log.push(result.1.clone()); + log.push(msg); } } // Update power after placing - let cores = game_state.game_map.tiles_of_type(tiles::TileType::PowerCore); - game_state.game_map.update_power(&cores); + let cores = game_map.tiles_of_type(tiles::TileType::PowerCore); + game_map.update_power(&cores); } - // X: demolish tile + // X: demolish tile — destructure game_state to split borrows if keyboard.just_pressed(KeyCode::KeyX) { - if let Some(ref mut bm) = game_state.build_mode { - let msg = bm.demolish_tile(&mut game_state.game_map, &mut game_state.player); - game_state.log.push(msg); - // Update power after demolishing - let cores = game_state.game_map.tiles_of_type(tiles::TileType::PowerCore); - game_state.game_map.update_power(&cores); - } + let GameState { + build_mode, + game_map, + player, + log, + .. + } = &mut *game_state; + + let mut bm = build_mode.take().unwrap(); + let msg = bm.demolish_tile(game_map, player); + *build_mode = Some(bm); + log.push(msg); + // Update power after demolishing + let cores = game_map.tiles_of_type(tiles::TileType::PowerCore); + game_map.update_power(&cores); } // ESC: exit build mode @@ -681,6 +694,18 @@ fn handle_input( if keyboard.just_pressed(KeyCode::Minus) || keyboard.just_pressed(KeyCode::NumpadSubtract) { game_state.defend_speed = game_state.defend_speed.saturating_add(25).min(500); } + + // Superpower activation (number keys 1-9) + let power_keys = [ + KeyCode::Digit1, KeyCode::Digit2, KeyCode::Digit3, + KeyCode::Digit4, KeyCode::Digit5, KeyCode::Digit6, + KeyCode::Digit7, KeyCode::Digit8, KeyCode::Digit9, + ]; + for (i, key) in power_keys.iter().enumerate() { + if keyboard.just_pressed(*key) { + activate_power(&mut game_state, i); + } + } } // Update cursor position to follow player (when not in build mode) @@ -724,6 +749,36 @@ fn start_wave(game_state: &mut ResMut) { gs.log.push(format!("=== WAVE {} INCOMING! ===", wave)); } +fn activate_power(game_state: &mut ResMut, index: usize) { + let gs = &mut **game_state; + if index >= gs.player.powers.len() { return; } + if !gs.player.powers[index].is_ready() { + gs.log.push(format!("{} is on cooldown ({}t)", gs.player.powers[index].name, gs.player.powers[index].current_cooldown)); + return; + } + + let power = gs.player.powers[index].clone(); + let messages = gs.combat.activate_power( + &power, + &mut gs.player, + &mut gs.agents, + &mut gs.minions, + &gs.game_map, + ); + + gs.player.powers[index].use_power(); + + for msg in &messages { + gs.log.push(msg.clone()); + } + + for kill in gs.combat.kill_events.clone() { + gs.player.gold += kill.gold_value; + gs.player.score += kill.gold_value; + gs.player.kills += 1; + } +} + // ─── Defend Phase ──────────────────────────────────────────────────────────── fn update_defend_phase( @@ -808,6 +863,9 @@ fn update_defend_phase( combat.process_medbay_healing(game_map, minions); combat.process_training_room(game_map, minions); + // Tick power cooldowns + player.tick_power_cooldowns(); + // Check wave end let all_dead = agents.iter().all(|a| !a.entity.alive); if all_dead || player_died { @@ -1191,8 +1249,23 @@ fn render_ui( text.push_str("SPACE: launch\n"); text.push_str("ESC: close\n"); } else if gs.phase == Phase::Defend { + // Show superpowers + if !gs.player.powers.is_empty() { + text.push_str("--- Powers ---\n"); + for (i, power) in gs.player.powers.iter().enumerate() { + if power.is_ready() { + text.push_str(&format!("[{}] {} READY\n", i + 1, power.name)); + } else { + text.push_str(&format!("[{}] {} ({}t)\n", i + 1, power.name, power.current_cooldown)); + } + } + if gs.player.is_invulnerable() { + text.push_str(&format!("SHIELD: {}t\n", gs.player.invulnerability_turns)); + } + } text.push_str("p: pause/resume\n"); text.push_str("+/-: speed\n"); + text.push_str("1-9: use power\n"); } else { // Normal build phase text.push_str("hjkl/arrows: move\n"); diff --git a/src/bin/terminal/input.rs b/src/bin/terminal/input.rs index 6a2fcc1..bbf15c3 100644 --- a/src/bin/terminal/input.rs +++ b/src/bin/terminal/input.rs @@ -43,6 +43,16 @@ pub enum Command { HireDown, HireSelect, HireClose, + // Superpowers + ActivatePower1, + ActivatePower2, + ActivatePower3, + ActivatePower4, + ActivatePower5, + ActivatePower6, + ActivatePower7, + ActivatePower8, + ActivatePower9, } pub fn handle_key( @@ -113,6 +123,15 @@ pub fn handle_key( KeyCode::Char('+') | KeyCode::Char('=') => Some(Command::SpeedUp), KeyCode::Char('-') => Some(Command::SpeedDown), KeyCode::Char(' ') | KeyCode::Char('p') => Some(Command::TogglePause), + KeyCode::Char('1') => Some(Command::ActivatePower1), + KeyCode::Char('2') => Some(Command::ActivatePower2), + KeyCode::Char('3') => Some(Command::ActivatePower3), + KeyCode::Char('4') => Some(Command::ActivatePower4), + KeyCode::Char('5') => Some(Command::ActivatePower5), + KeyCode::Char('6') => Some(Command::ActivatePower6), + KeyCode::Char('7') => Some(Command::ActivatePower7), + KeyCode::Char('8') => Some(Command::ActivatePower8), + KeyCode::Char('9') => Some(Command::ActivatePower9), KeyCode::Char('q') => Some(Command::Quit), _ => None, }; diff --git a/src/bin/terminal/mod.rs b/src/bin/terminal/mod.rs index c83f774..1453267 100644 --- a/src/bin/terminal/mod.rs +++ b/src/bin/terminal/mod.rs @@ -53,9 +53,10 @@ impl Game { fn new() -> Self { let game_map = GameMap::new(80, 40); let throne = game_map.throne_pos().unwrap_or((40, 20)); - let player = Player::new(throne.0, throne.1); + let mut player = Player::new(throne.0, throne.1); + player.update_powers(); // Initialize available powers - // Start with 3 workers near the throne (like Evil Genius) + // Start with 3 workers near the throne (like Supervillain) let mut starting_minions = Vec::new(); for (dx, dy) in [(1, 0), (-1, 0), (0, 1)] { let mx = throne.0 + dx; @@ -268,6 +269,7 @@ impl Game { } self.ui.add_log(&format!("Researched: {name}!")); + self.player.update_powers(); // Check if new powers unlocked } } } @@ -431,6 +433,30 @@ impl Game { Command::HireClose => { self.hire_menu = false; } + // Superpowers — activate by number key during defend phase + cmd @ (Command::ActivatePower1 + | Command::ActivatePower2 + | Command::ActivatePower3 + | Command::ActivatePower4 + | Command::ActivatePower5 + | Command::ActivatePower6 + | Command::ActivatePower7 + | Command::ActivatePower8 + | Command::ActivatePower9) => { + let index = match cmd { + Command::ActivatePower1 => 0, + Command::ActivatePower2 => 1, + Command::ActivatePower3 => 2, + Command::ActivatePower4 => 3, + Command::ActivatePower5 => 4, + Command::ActivatePower6 => 5, + Command::ActivatePower7 => 6, + Command::ActivatePower8 => 7, + Command::ActivatePower9 => 8, + _ => 0, + }; + self.activate_power(index); + } _ => {} } } @@ -561,7 +587,7 @@ impl Game { if throne_reached { self.player.entity.take_damage(15); - self.ui.add_log("The Overlord takes 15 damage!"); + self.ui.add_log("The Supervillain takes 15 damage!"); if !self.player.entity.alive { self.phase = Phase::GameOver; return; @@ -617,6 +643,9 @@ impl Game { // Cleanup self.combat.cleanup_dead(&mut self.minions, &mut self.agents, &mut self.henchmen); + // Tick power cooldowns + self.player.tick_power_cooldowns(); + // Check if wave is over if self.combat.all_agents_dead(&self.agents) { self.ui.add_log(&format!("=== WAVE {} CLEARED! ===", self.player.wave)); @@ -643,7 +672,7 @@ impl Game { // Process active schemes self.tick_schemes(); - // Heal the overlord a bit + // Heal the Supervillain a bit self.player.entity.heal(10); } } @@ -915,6 +944,39 @@ impl Game { Ok(()) } + + fn activate_power(&mut self, index: usize) { + if index >= self.player.powers.len() { return; } + if !self.player.powers[index].is_ready() { + self.ui.add_log(&format!("{} is on cooldown ({}t)", self.player.powers[index].name, self.player.powers[index].current_cooldown)); + return; + } + + // Clone the power data to avoid borrow issues + let power = self.player.powers[index].clone(); + let messages = self.combat.activate_power( + &power, + &mut self.player, + &mut self.agents, + &mut self.minions, + &self.game_map, + ); + + // Apply the power (set cooldown) + self.player.powers[index].use_power(); + + // Log messages + for msg in &messages { + self.ui.add_log(msg); + } + + // Apply kill events from power + for kill in self.combat.kill_events.clone() { + self.player.gold += kill.gold_value; + self.player.score += kill.gold_value; + self.player.kills += 1; + } + } } fn main() -> io::Result<()> { diff --git a/src/bin/terminal/ui.rs b/src/bin/terminal/ui.rs index f9cfcd3..7e5213d 100644 --- a/src/bin/terminal/ui.rs +++ b/src/bin/terminal/ui.rs @@ -220,7 +220,7 @@ impl UI { write(stdout, &mut row, &format!("Research: {}", player.research), Color::Blue, false)?; row += 1; - write(stdout, &mut row, "--- Evil Genius ---", Color::Yellow, false)?; + write(stdout, &mut row, "--- Supervillain ---", Color::Yellow, false)?; write(stdout, &mut row, &format!("HP: {}/{}", player.entity.hp, player.entity.max_hp), Color::Magenta, false)?; // Heat bar let heat_color = if player.heat >= 60 { Color::Red } else if player.heat >= 30 { Color::Yellow } else { Color::Green }; @@ -263,6 +263,24 @@ impl UI { } row += 1; + // Superpowers + if !player.powers.is_empty() { + write(stdout, &mut row, "--- Powers ---", Color::Magenta, false)?; + for (i, power) in player.powers.iter().enumerate() { + let status = if power.is_ready() { + format!("[{}] {} READY", i + 1, power.name) + } else { + format!("[{}] {} ({}t)", i + 1, power.name, power.current_cooldown) + }; + let color = if power.is_ready() { Color::Magenta } else { Color::DarkGrey }; + write(stdout, &mut row, &status, color, false)?; + } + if player.is_invulnerable() { + write(stdout, &mut row, &format!("SHIELD: {}t", player.invulnerability_turns), Color::Cyan, false)?; + } + row += 1; + } + // Active schemes if !player.active_schemes.is_empty() { write(stdout, &mut row, "--- Schemes ---", Color::DarkYellow, false)?; @@ -661,7 +679,7 @@ impl UI { let messages: [(&str, Color, bool); 8] = [ ("GAME OVER", Color::Red, true), ("", Color::White, false), - ("The Evil Genius has fallen!", Color::White, false), + ("The Supervillain has fallen!", Color::White, false), ("", Color::White, false), ("", Color::White, false), ("", Color::White, false), diff --git a/src/combat.rs b/src/combat.rs index 4f13928..e2340bd 100644 --- a/src/combat.rs +++ b/src/combat.rs @@ -155,9 +155,24 @@ impl CombatSystem { // Check if agent reached throne if (agents[i].entity.x, agents[i].entity.y) == throne { - self.log_msg(&format!("{} reaches the throne room!", agents[i].entity.name)); - agents[i].entity.alive = false; - return true; // throne reached + if player.is_invulnerable() { + self.log_msg(&format!("{} is repelled by the Supervillain's shield!", agents[i].entity.name)); + // Push agent back + let dx = agents[i].entity.x - throne.0; + let dy = agents[i].entity.y - throne.1; + let push = if dx != 0 || dy != 0 { 3 } else { 3 }; + let new_x = (agents[i].entity.x + dx.signum() * push).clamp(0, game_map.width - 1); + let new_y = (agents[i].entity.y + dy.signum() * push).clamp(0, game_map.height - 1); + if game_map.is_walkable(new_x, new_y) { + agents[i].entity.x = new_x; + agents[i].entity.y = new_y; + } + agents[i].entity.stun_turns = agents[i].entity.stun_turns.max(1); + } else { + self.log_msg(&format!("{} reaches the throne room!", agents[i].entity.name)); + agents[i].entity.alive = false; + return true; // throne reached + } } // Thief steals from vaults/loot rooms @@ -485,6 +500,195 @@ impl CombatSystem { else if self.trap_combo_count >= 2 { 25 } else { 0 } } + + /// Activate a superpower. Returns log messages for the action. + pub fn activate_power( + &mut self, + power: &crate::superpowers::Superpower, + player: &mut Player, + agents: &mut [Agent], + minions: &mut Vec, + game_map: &GameMap, + ) -> Vec { + let mut messages = Vec::new(); + + match power.effect { + crate::superpowers::SuperpowerEffect::AoeDamage { damage } => { + let px = player.entity.x; + let py = player.entity.y; + let range = power.range; + let mut hit_count = 0; + for agent in agents.iter_mut() { + if !agent.entity.alive { continue; } + if agent.entity.distance_to(px, py) <= range { + agent.entity.hp -= damage; + hit_count += 1; + if agent.entity.hp <= 0 { + agent.entity.alive = false; + self.kill_events.push(KillEvent { + gold_value: 25, + agent_type: agent.agent_type.clone(), + }); + } + } + } + messages.push(format!("{} hits {} agents for {} damage!", power.name, hit_count, damage)); + } + + crate::superpowers::SuperpowerEffect::AoeStun { turns } => { + let px = player.entity.x; + let py = player.entity.y; + let range = power.range; + let mut hit_count = 0; + for agent in agents.iter_mut() { + if !agent.entity.alive { continue; } + if agent.entity.distance_to(px, py) <= range { + agent.entity.stun_turns = agent.entity.stun_turns.max(turns); + hit_count += 1; + } + } + messages.push(format!("{} stuns {} agents for {} turns!", power.name, hit_count, turns)); + } + + crate::superpowers::SuperpowerEffect::AoePoison { turns, damage } => { + let px = player.entity.x; + let py = player.entity.y; + let range = power.range; + let mut hit_count = 0; + for agent in agents.iter_mut() { + if !agent.entity.alive { continue; } + if agent.entity.distance_to(px, py) <= range { + agent.entity.poison_turns = agent.entity.poison_turns.max(turns); + agent.entity.poison_dmg = agent.entity.poison_dmg.max(damage); + hit_count += 1; + } + } + messages.push(format!("{} poisons {} agents for {} dmg/turn!", power.name, hit_count, damage)); + } + + crate::superpowers::SuperpowerEffect::SelfHeal { amount } => { + let healed = amount.min(player.entity.max_hp - player.entity.hp); + player.entity.hp += healed; + messages.push(format!("{} heals for {} HP! ({} HP now)", power.name, healed, player.entity.hp)); + } + + crate::superpowers::SuperpowerEffect::SummonGuards { count } => { + let px = player.entity.x; + let py = player.entity.y; + let mut spawned = 0; + for dx in -1..=1 { + for dy in -1..=1 { + if spawned >= count { break; } + if dx == 0 && dy == 0 { continue; } + let nx = px + dx; + let ny = py + dy; + if game_map.is_walkable(nx, ny) { + minions.push(crate::entities::Minion::new( + nx, ny, + crate::entities::MinionClass::Guard, + player.minion_hp_bonus, + player.minion_damage_bonus, + )); + spawned += 1; + } + } + } + messages.push(format!("{} summons {} guards!", power.name, spawned)); + } + + crate::superpowers::SuperpowerEffect::LineDamage { damage, length } => { + // Fire in the direction the player last moved (or right by default) + let px = player.entity.x; + let py = player.entity.y; + let mut hit_count = 0; + // Try all 4 cardinal directions and hit agents in the line + for i in 1..=length { + for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1)] { + let tx = px + dx * i; + let ty = py + dy * i; + for agent in agents.iter_mut() { + if !agent.entity.alive { continue; } + if agent.entity.x == tx && agent.entity.y == ty { + agent.entity.hp -= damage; + hit_count += 1; + if agent.entity.hp <= 0 { + agent.entity.alive = false; + self.kill_events.push(KillEvent { + gold_value: 25, + agent_type: agent.agent_type.clone(), + }); + } + } + } + } + } + messages.push(format!("{} hits {} agents for {} damage!", power.name, hit_count, damage)); + } + + crate::superpowers::SuperpowerEffect::Repulsion { distance } => { + let px = player.entity.x; + let py = player.entity.y; + let range = power.range; + let mut pushed_count = 0; + for agent in agents.iter_mut() { + if !agent.entity.alive { continue; } + if agent.entity.distance_to(px, py) > range { continue; } + let dx = agent.entity.x - px; + let dy = agent.entity.y - py; + if dx == 0 && dy == 0 { continue; } + let len = (dx.abs() + dy.abs()).max(1); + let push_x = (dx * distance / len).signum() * distance; + let push_y = (dy * distance / len).signum() * distance; + let new_x = (agent.entity.x + push_x).clamp(0, game_map.width - 1); + let new_y = (agent.entity.y + push_y).clamp(0, game_map.height - 1); + if game_map.is_walkable(new_x, new_y) { + agent.entity.x = new_x; + agent.entity.y = new_y; + } + pushed_count += 1; + } + messages.push(format!("{} pushes {} agents away!", power.name, pushed_count)); + } + + crate::superpowers::SuperpowerEffect::DrainLife { damage_per_enemy, heal_per_enemy } => { + let px = player.entity.x; + let py = player.entity.y; + let range = power.range; + let mut hit_count = 0; + for agent in agents.iter_mut() { + if !agent.entity.alive { continue; } + if agent.entity.distance_to(px, py) <= range { + agent.entity.hp -= damage_per_enemy; + hit_count += 1; + if agent.entity.hp <= 0 { + agent.entity.alive = false; + self.kill_events.push(KillEvent { + gold_value: 25, + agent_type: agent.agent_type.clone(), + }); + } + } + } + let total_heal = hit_count * heal_per_enemy; + let healed = total_heal.min(player.entity.max_hp - player.entity.hp); + player.entity.hp += healed; + messages.push(format!("{} drains {} enemies, healing {} HP!", power.name, hit_count, healed)); + } + + crate::superpowers::SuperpowerEffect::Invulnerability { turns } => { + player.invulnerability_turns = turns; + messages.push(format!("{} — invulnerable for {} turns!", power.name, turns)); + } + + crate::superpowers::SuperpowerEffect::Teleport => { + player.teleport_mode = true; + messages.push(format!("{} — select a destination!", power.name)); + } + } + + self.log.extend(messages.clone()); + messages + } } #[cfg(test)] @@ -642,4 +846,97 @@ mod tests { // Worker should NOT repair the trap assert!(!map.powered.contains(&(11, 10))); } + + #[test] + fn superpower_evil_laugh_stuns_agents() { + let mut cs = CombatSystem::new(); + let mut player = Player::new(40, 20); + let mut agents = vec![ + Agent::new(41, 20, "investigator", 0), + Agent::new(42, 20, "investigator", 0), + ]; + let mut minions = Vec::new(); + let map = GameMap::new(80, 40); + + let power = crate::superpowers::Superpower { + name: "Evil Laugh", + description: "test", + cooldown: 5, + current_cooldown: 0, + unlock_key: "test", + unlock_notoriety: 0, + range: 5, + effect: crate::superpowers::SuperpowerEffect::AoeStun { turns: 2 }, + }; + + let msgs = cs.activate_power(&power, &mut player, &mut agents, &mut minions, &map); + assert!(msgs[0].contains("stuns")); + assert_eq!(agents[0].entity.stun_turns, 2); + assert_eq!(agents[1].entity.stun_turns, 2); + } + + #[test] + fn superpower_death_ray_kills_agents() { + let mut cs = CombatSystem::new(); + let mut player = Player::new(40, 20); + let mut agents = vec![ + Agent::new(41, 20, "investigator", 0), + ]; + let mut minions = Vec::new(); + let map = GameMap::new(80, 40); + + let power = crate::superpowers::Superpower { + name: "Death Ray", + description: "test", + cooldown: 8, + current_cooldown: 0, + unlock_key: "test", + unlock_notoriety: 0, + range: 8, + effect: crate::superpowers::SuperpowerEffect::LineDamage { damage: 25, length: 8 }, + }; + + cs.activate_power(&power, &mut player, &mut agents, &mut minions, &map); + assert!(!agents[0].entity.alive); + } + + #[test] + fn superpower_self_heal() { + let mut cs = CombatSystem::new(); + let mut player = Player::new(40, 20); + player.entity.hp = 50; + let mut agents: Vec = Vec::new(); + let mut minions = Vec::new(); + let map = GameMap::new(80, 40); + + let power = crate::superpowers::Superpower { + name: "Dark Regeneration", + description: "test", + cooldown: 6, + current_cooldown: 0, + unlock_key: "test", + unlock_notoriety: 0, + range: 0, + effect: crate::superpowers::SuperpowerEffect::SelfHeal { amount: 30 }, + }; + + cs.activate_power(&power, &mut player, &mut agents, &mut minions, &map); + assert_eq!(player.entity.hp, 80); // 50 + 30 + } + + #[test] + fn invulnerability_blocks_throne_attack() { + let mut cs = CombatSystem::new(); + let mut player = Player::new(40, 20); + player.invulnerability_turns = 3; + let mut agents = vec![ + Agent::new(40, 20, "soldier", 0), + ]; + let mut minions = Vec::new(); + let mut henchmen = Vec::new(); + let mut map = GameMap::new(80, 40); + + let died = cs.process_agent_turn(&mut map, &mut player, &mut agents, &mut minions, &mut henchmen, (40, 20)); + assert!(!died); // Should be blocked by invulnerability + } } diff --git a/src/entities/mod.rs b/src/entities/mod.rs index f1580f0..b278942 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -2,7 +2,7 @@ //! //! Module layout: //! - `core` — Base Entity struct (position, HP, status effects) -//! - `player` — The Evil Genius + scheme tracking +//! - `player` — The Supervillain + scheme tracking //! - `minion` — Minion classes (Worker/Guard/Technician/Scientist) //! - `henchman` — Named lieutenants with special abilities //! - `agent` — Enemy agents (Investigator/Thief/Saboteur/Soldier/Super Agent) diff --git a/src/entities/player.rs b/src/entities/player.rs index 15e76c6..a923e55 100644 --- a/src/entities/player.rs +++ b/src/entities/player.rs @@ -1,8 +1,9 @@ -//! The player-controlled Evil Genius + active scheme tracking. +//! The player-controlled Supervillain + active scheme tracking. use std::collections::HashSet; use super::core::Entity; +use crate::superpowers::Superpower; #[derive(Debug, Clone)] pub struct Player { @@ -32,6 +33,10 @@ pub struct Player { // Collection pub loot: Vec, pub active_schemes: Vec, + // Superpowers + pub powers: Vec, + pub invulnerability_turns: i32, + pub teleport_mode: bool, // When true, next click/keypress is teleport destination } #[derive(Debug, Clone)] @@ -65,7 +70,7 @@ impl Player { Self { entity: Entity { x, y, - name: "The Evil Genius".to_string(), + name: "The Supervillain".to_string(), max_hp: 100, hp: 100, damage: 15, char: '@', @@ -89,9 +94,32 @@ impl Player { heat: 0, notoriety: 0, gold_income: 0, loot: Vec::new(), active_schemes: Vec::new(), + powers: Vec::new(), + invulnerability_turns: 0, + teleport_mode: false, } } + /// Update unlocked powers based on current unlocks and notoriety. + pub fn update_powers(&mut self) { + self.powers = crate::superpowers::get_unlocked_powers(&self.unlocked, self.notoriety); + } + + /// Tick cooldowns on all powers. + pub fn tick_power_cooldowns(&mut self) { + for power in &mut self.powers { + power.tick_cooldown(); + } + if self.invulnerability_turns > 0 { + self.invulnerability_turns -= 1; + } + } + + /// Check if the Supervillain is currently invulnerable. + pub fn is_invulnerable(&self) -> bool { + self.invulnerability_turns > 0 + } + pub fn add_heat(&mut self, amount: i32) { self.heat = (self.heat + amount).max(0); } diff --git a/src/lib.rs b/src/lib.rs index 8ba0319..cb02100 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod tiles; pub mod map; pub mod combat; pub mod build; +pub mod superpowers; diff --git a/src/main.rs b/src/main.rs index d739453..f392c07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,7 @@ impl Game { let throne = game_map.throne_pos().unwrap_or((40, 20)); let player = Player::new(throne.0, throne.1); - // Start with 3 workers near the throne (like Evil Genius) + // Start with 3 workers near the throne (like Supervillain) let mut starting_minions = Vec::new(); for (dx, dy) in [(1, 0), (-1, 0), (0, 1)] { let mx = throne.0 + dx; @@ -565,7 +565,7 @@ impl Game { if throne_reached { self.player.entity.take_damage(15); - self.ui.add_log("The Overlord takes 15 damage!"); + self.ui.add_log("The Supervillain takes 15 damage!"); if !self.player.entity.alive { self.phase = Phase::GameOver; return; @@ -647,7 +647,7 @@ impl Game { // Process active schemes self.tick_schemes(); - // Heal the overlord a bit + // Heal the Supervillain a bit self.player.entity.heal(10); } } diff --git a/src/superpowers.rs b/src/superpowers.rs new file mode 100644 index 0000000..9380276 --- /dev/null +++ b/src/superpowers.rs @@ -0,0 +1,260 @@ +//! Supervillain superpowers — special abilities unlocked through research/notoriety. +//! +//! Powers are activated during the defend phase with number keys (1-9). + +use std::collections::HashSet; + +/// A superpower the Supervillain can use. +#[derive(Debug, Clone)] +pub struct Superpower { + pub name: &'static str, + pub description: &'static str, + pub cooldown: i32, + pub current_cooldown: i32, + pub unlock_key: &'static str, + pub unlock_notoriety: i32, + pub range: i32, + pub effect: SuperpowerEffect, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SuperpowerEffect { + /// Deal damage to all enemies in range + AoeDamage { damage: i32 }, + /// Stun all enemies in range for N turns + AoeStun { turns: i32 }, + /// Poison all enemies in range for N turns (damage per turn) + AoePoison { turns: i32, damage: i32 }, + /// Heal self by amount + SelfHeal { amount: i32 }, + /// Spawn N emergency guards around the Supervillain + SummonGuards { count: i32 }, + /// Deal damage in a line from player + LineDamage { damage: i32, length: i32 }, + /// Push all enemies away from the Supervillain + Repulsion { distance: i32 }, + /// Drain HP from nearby enemies, healing self + DrainLife { damage_per_enemy: i32, heal_per_enemy: i32 }, + /// Become invulnerable for N turns + Invulnerability { turns: i32 }, + /// Teleport to any visible tile + Teleport, +} + +impl Superpower { + pub fn is_ready(&self) -> bool { + self.current_cooldown == 0 + } + + pub fn tick_cooldown(&mut self) { + if self.current_cooldown > 0 { + self.current_cooldown -= 1; + } + } + + pub fn use_power(&mut self) { + self.current_cooldown = self.cooldown; + } +} + +/// All available superpowers. +pub fn all_superpowers() -> Vec { + vec![ + Superpower { + name: "Evil Laugh", + description: "Stun all agents within 5 tiles for 2 turns", + cooldown: 5, + current_cooldown: 0, + unlock_key: "power_evil_laugh", + unlock_notoriety: 10, + range: 5, + effect: SuperpowerEffect::AoeStun { turns: 2 }, + }, + Superpower { + name: "Death Ray", + description: "Fire a beam dealing 25 damage in a line (8 tiles)", + cooldown: 8, + current_cooldown: 0, + unlock_key: "power_death_ray", + unlock_notoriety: 30, + range: 8, + effect: SuperpowerEffect::LineDamage { damage: 25, length: 8 }, + }, + Superpower { + name: "Dark Regeneration", + description: "Heal yourself for 30 HP", + cooldown: 6, + current_cooldown: 0, + unlock_key: "power_regen", + unlock_notoriety: 15, + range: 0, + effect: SuperpowerEffect::SelfHeal { amount: 30 }, + }, + Superpower { + name: "Emergency Reinforcements", + description: "Summon 2 guards at your position", + cooldown: 10, + current_cooldown: 0, + unlock_key: "power_summon", + unlock_notoriety: 25, + range: 0, + effect: SuperpowerEffect::SummonGuards { count: 2 }, + }, + Superpower { + name: "Force Wave", + description: "Push all agents 4 tiles away from you", + cooldown: 7, + current_cooldown: 0, + unlock_key: "power_force_wave", + unlock_notoriety: 20, + range: 6, + effect: SuperpowerEffect::Repulsion { distance: 4 }, + }, + Superpower { + name: "Soul Drain", + description: "Drain 10 HP from each nearby enemy (range 4), healing 5 per enemy", + cooldown: 6, + current_cooldown: 0, + unlock_key: "power_drain", + unlock_notoriety: 35, + range: 4, + effect: SuperpowerEffect::DrainLife { damage_per_enemy: 10, heal_per_enemy: 5 }, + }, + Superpower { + name: "Impenetrable Shield", + description: "Become invulnerable for 3 turns", + cooldown: 12, + current_cooldown: 0, + unlock_key: "power_shield", + unlock_notoriety: 50, + range: 0, + effect: SuperpowerEffect::Invulnerability { turns: 3 }, + }, + Superpower { + name: "Blink", + description: "Teleport to any visible tile", + cooldown: 4, + current_cooldown: 0, + unlock_key: "power_blink", + unlock_notoriety: 40, + range: 0, + effect: SuperpowerEffect::Teleport, + }, + Superpower { + name: "Plague Cloud", + description: "Poison all agents within 6 tiles for 3 turns (5 damage/turn)", + cooldown: 9, + current_cooldown: 0, + unlock_key: "power_plague", + unlock_notoriety: 45, + range: 6, + effect: SuperpowerEffect::AoePoison { turns: 3, damage: 5 }, + }, + ] +} + +/// Get powers unlocked by the player (based on unlocks and notoriety). +pub fn get_unlocked_powers(player_unlocks: &HashSet, notoriety: i32) -> Vec { + all_superpowers() + .into_iter() + .filter(|p| { + player_unlocks.contains(p.unlock_key) && notoriety >= p.unlock_notoriety + }) + .collect() +} + +/// Research unlock entries for superpowers. +pub fn power_research_unlocks() -> Vec<(&'static str, i32, &'static str)> { + vec![ + ("power_evil_laugh", 50, "Evil Laugh (Stun power)"), + ("power_regen", 80, "Dark Regeneration (Self-heal)"), + ("power_force_wave", 120, "Force Wave (Repulsion)"), + ("power_summon", 150, "Emergency Reinforcements (Summon guards)"), + ("power_death_ray", 200, "Death Ray (Line damage)"), + ("power_drain", 180, "Soul Drain (Life steal)"), + ("power_blink", 220, "Blink (Teleport)"), + ("power_plague", 250, "Plague Cloud (AoE poison)"), + ("power_shield", 300, "Impenetrable Shield (Invulnerability)"), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_powers_have_valid_data() { + let powers = all_superpowers(); + assert!(powers.len() >= 8); + for p in &powers { + assert!(!p.name.is_empty()); + assert!(!p.description.is_empty()); + assert!(p.cooldown > 0); + assert!(p.unlock_notoriety >= 0); + assert!(!p.unlock_key.is_empty()); + } + } + + #[test] + fn cooldown_ticks_down() { + let mut power = Superpower { + name: "Test", + description: "Test power", + cooldown: 3, + current_cooldown: 3, + unlock_key: "test", + unlock_notoriety: 0, + range: 5, + effect: SuperpowerEffect::AoeDamage { damage: 10 }, + }; + assert!(!power.is_ready()); + power.tick_cooldown(); + assert_eq!(power.current_cooldown, 2); + power.tick_cooldown(); + power.tick_cooldown(); + assert!(power.is_ready()); + } + + #[test] + fn using_power_resets_cooldown() { + let mut power = Superpower { + name: "Test", + description: "Test power", + cooldown: 5, + current_cooldown: 0, + unlock_key: "test", + unlock_notoriety: 0, + range: 5, + effect: SuperpowerEffect::AoeDamage { damage: 10 }, + }; + assert!(power.is_ready()); + power.use_power(); + assert!(!power.is_ready()); + assert_eq!(power.current_cooldown, 5); + } + + #[test] + fn get_unlocked_filters_correctly() { + let mut unlocks = HashSet::new(); + unlocks.insert("power_evil_laugh".to_string()); + unlocks.insert("power_regen".to_string()); + + let powers = get_unlocked_powers(&unlocks, 20); + // Evil Laugh needs notoriety 10, regen needs 15 + assert_eq!(powers.len(), 2); + + let low_notoriety = get_unlocked_powers(&unlocks, 5); + assert_eq!(low_notoriety.len(), 0); + } + + #[test] + fn research_unlocks_not_empty() { + let unlocks = power_research_unlocks(); + assert!(!unlocks.is_empty()); + for (key, cost, name) in &unlocks { + assert!(!key.is_empty()); + assert!(*cost > 0); + assert!(!name.is_empty()); + } + } +} diff --git a/src/tiles.rs b/src/tiles.rs index 9b4039a..35bba17 100644 --- a/src/tiles.rs +++ b/src/tiles.rs @@ -343,6 +343,16 @@ pub fn research_unlocks() -> Vec<(&'static str, i32, &'static str)> { ("minion_weapons", 120, "Minion Weapons (+50% dmg)"), ("scientist_class", 80, "Scientist Minion Class"), ("technician_class", 100, "Technician Minion Class"), + // Superpowers + ("power_evil_laugh", 50, "Evil Laugh (Stun power)"), + ("power_regen", 80, "Dark Regeneration (Self-heal)"), + ("power_force_wave", 120, "Force Wave (Repulsion)"), + ("power_summon", 150, "Emergency Reinforcements (Summon guards)"), + ("power_death_ray", 200, "Death Ray (Line damage)"), + ("power_drain", 180, "Soul Drain (Life steal)"), + ("power_blink", 220, "Blink (Teleport)"), + ("power_plague", 250, "Plague Cloud (AoE poison)"), + ("power_shield", 300, "Impenetrable Shield (Invulnerability)"), ] } diff --git a/src/ui.rs b/src/ui.rs index 01b56c7..61a239e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -203,7 +203,7 @@ impl UI { Ok(()) }; - write(stdout, &mut row, "EVIL GENIUS", Color::Magenta, true)?; + write(stdout, &mut row, "SUPERVILLAIN", Color::Magenta, true)?; row += 1; write(stdout, &mut row, &format!("Phase: {}", phase.to_uppercase()), Color::Yellow, true)?; let wave_info = if phase == "defend" { @@ -220,7 +220,7 @@ impl UI { write(stdout, &mut row, &format!("Research: {}", player.research), Color::Blue, false)?; row += 1; - write(stdout, &mut row, "--- Evil Genius ---", Color::Yellow, false)?; + write(stdout, &mut row, "--- Supervillain ---", Color::Yellow, false)?; write(stdout, &mut row, &format!("HP: {}/{}", player.entity.hp, player.entity.max_hp), Color::Magenta, false)?; // Heat bar let heat_color = if player.heat >= 60 { Color::Red } else if player.heat >= 30 { Color::Yellow } else { Color::Green }; @@ -661,7 +661,7 @@ impl UI { let messages: [(&str, Color, bool); 8] = [ ("GAME OVER", Color::Red, true), ("", Color::White, false), - ("The Evil Genius has fallen!", Color::White, false), + ("The Supervillain has fallen!", Color::White, false), ("", Color::White, false), ("", Color::White, false), ("", Color::White, false), -- 2.51.2