From 2640bea49e81376c3c17a4e659dc07443a79955d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 5 Nov 2021 23:06:03 +0100 Subject: [PATCH] Targeting: the Fireball --- src/main.rs | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 232 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7f16e88..6654bd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,11 @@ const MSG_WIDTH: i32 = SCREEN_WIDTH - BAR_WIDTH - 2; const MSG_HEIGHT: usize = PANEL_HEIGHT as usize - 1; const MAX_ROOM_ITEMS: i32 = 2; const INVENTORY_WIDTH: i32 = 50; +const HEAL_AMOUNT: i32 = 4; +const LIGHTNING_DAMAGE: i32 = 40; +const LIGHTNING_RANGE: i32 = 5; +const CONFUSE_RANGE: i32 = 8; +const CONFUSE_NUM_TURNS: i32 = 10; struct Tcod { root: Root, @@ -150,6 +155,15 @@ impl Object { ); } } + + pub fn heal(&mut self, amount: i32) { + if let Some(ref mut fighter) = self.fighter { + fighter.hp += amount; + if fighter.hp > fighter.max_hp { + fighter.hp = fighter.max_hp; + } + } + } } #[derive(Clone, Copy, Debug)] @@ -255,6 +269,10 @@ enum PlayerAction { #[derive(Clone, Debug, PartialEq)] enum Ai { Basic, + Confused { + previous_ai: Box, + num_turns: i32, + }, } #[derive(Clone, Copy, Debug, PartialEq)] @@ -277,6 +295,13 @@ impl DeathCallback { #[derive(Clone, Copy, Debug, PartialEq)] enum Item { Heal, + Lightning, + Confuse, +} + +enum UseResult { + UsedUp, + Cancelled, } fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) -> PlayerAction { @@ -332,12 +357,15 @@ fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) -> P (Key { code: Text, .. }, "i", true) => { // Show the inventory - inventory_menu( + let inventory_index = inventory_menu( &game.inventory, "Press the key next to an item to use it, or any other to cancel.\n", &mut tcod.root, ); - TookTurn + if let Some(inventory_index) = inventory_index { + use_item(inventory_index, tcod, game, objects); + } + DidntTakeTurn } _ => DidntTakeTurn, @@ -460,10 +488,25 @@ fn place_objects(room: Rect, map: &Map, objects: &mut Vec) { // Only place it if the tile is note blocked if !is_blocked(x, y, map, objects) { - // Create a healing potion - let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false); - object.item = Some(Item::Heal); - objects.push(object); + let dice = rand::random::(); + let item = if dice < 0.7 { + // Create a healing potion (70% chance) + let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false); + object.item = Some(Item::Heal); + object + } else if dice < 0.7 + 0.1 { + // Create a lightning bolt scroll (10% chance) + let mut object = Object::new(x, y, '#', "scroll of lightning bolt", LIGHT_YELLOW, false); + object.item = Some(Item::Lightning); + object + } else { + // Create a confuse scroll (10% chance) + let mut object = Object::new(x, y, '#', "scroll of confusion", LIGHT_YELLOW, false); + object.item = Some(Item::Confuse); + object + }; + + objects.push(item); } } } @@ -522,6 +565,21 @@ fn move_towards(id: usize, target_x: i32, target_y: i32, map: &Map, objects: &mu } fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) { + use Ai::*; + + if let Some(ai) = objects[monster_id].ai.take() { + let new_ai = match ai { + Basic => ai_basic(monster_id, tcod, game, objects), + Confused { + previous_ai, + num_turns, + } => ai_confused(monster_id, tcod, game, objects, previous_ai, num_turns), + }; + objects[monster_id].ai = Some(new_ai); + } +} + +fn ai_basic(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) -> Ai { // A basic monster takes its turn. If you can see it, it can see you. let (monster_x, monster_y) = objects[monster_id].pos(); if tcod.fov.is_in_fov(monster_x, monster_y) { @@ -535,6 +593,40 @@ fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [ monster.attack(player, game); } } + Ai::Basic +} + +fn ai_confused( + monster_id: usize, + _tcod: &Tcod, + game: &mut Game, + objects: &mut [Object], + previous_ai: Box, + num_turns: i32, +) -> Ai { + if num_turns >= 0 { + // Still confused ... + // Move in a random direction, and decrease the number of turns confused + move_by( + monster_id, + rand::thread_rng().gen_range(-1, 2), + rand::thread_rng().gen_range(-1, 2), + &game.map, + objects, + ); + + Ai::Confused { + previous_ai: previous_ai, + num_turns: num_turns - 1, + } + } else { + // Restore previous AI (this one will be deleted) + game.messages.add( + format!("The {} is no longer confused!", objects[monster_id].name), + RED, + ); + *previous_ai + } } fn mut_two(first_index: usize, second_index: usize, items: &mut [T]) -> (&mut T, &mut T) { @@ -716,6 +808,140 @@ fn pick_item_up(object_id: usize, game: &mut Game, objects: &mut Vec) { } } +fn use_item(inventory_id: usize, tcod: &mut Tcod, game: &mut Game, objects: &mut [Object]) { + use Item::*; + // just call the "use_function" if it is defined + if let Some(item) = game.inventory[inventory_id].item { + let on_use = match item { + Heal => cast_heal, + Lightning => cast_lightning, + Confuse => cast_confuse, + }; + match on_use(inventory_id, tcod, game, objects) { + UseResult::UsedUp => { + // Destroy after use, unless it was cancelled for some reason + game.inventory.remove(inventory_id); + } + UseResult::Cancelled => { + game.messages.add("Cancelled", WHITE); + } + } + } else { + game.messages.add( + format!("The {} cannot be used.", game.inventory[inventory_id].name), + WHITE, + ); + } +} + +fn cast_heal( + _inventory_id: usize, + _tcod: &mut Tcod, + game: &mut Game, + objects: &mut [Object], +) -> UseResult { + // Heal the player + if let Some(fighter) = objects[PLAYER].fighter { + if fighter.hp == fighter.max_hp { + game.messages.add("You are already at full health.", RED); + return UseResult::Cancelled; + } + + game + .messages + .add("Your wounds start to feel better!", LIGHT_VIOLET); + objects[PLAYER].heal(HEAL_AMOUNT); + return UseResult::UsedUp; + } + UseResult::Cancelled +} + +fn cast_lightning( + _inventory_id: usize, + tcod: &mut Tcod, + game: &mut Game, + objects: &mut [Object], +) -> UseResult { + // Find closest enemy (inside a maximum range) and damage it + let monster_id = closest_monster(tcod, objects, LIGHTNING_RANGE); + if let Some(monster_id) = monster_id { + // Zap it ! + game.messages.add( + format!( + "A lightning bolt strikes the {} with a loud thunder! \ + The damage is {} hit points.", + objects[monster_id].name, LIGHTNING_DAMAGE + ), + LIGHT_BLUE, + ); + objects[monster_id].take_damage(LIGHTNING_DAMAGE, game); + UseResult::UsedUp + } else { + // No enemy found within maximum range + game + .messages + .add("No enemy is close enough to strike.", RED); + + UseResult::Cancelled + } +} + +fn cast_confuse( + _inventory_id: usize, + tcod: &mut Tcod, + game: &mut Game, + objects: &mut [Object], +) -> UseResult { + let monster_id = target_monster(CONFUSE_RANGE, objects, tcod); + if let Some(monster_id) = monster_id { + let old_ai = objects[monster_id].ai.take().unwrap_or(Ai::Basic); + // Replace the monster's AI with a "confused" one; after + // some turns it will restore the old AI + objects[monster_id].ai = Some(Ai::Confused { + previous_ai: Box::new(old_ai), + num_turns: CONFUSE_NUM_TURNS, + }); + game.messages.add( + format!( + "The eyes of {} look vacant, as he starts to stumble around!", + objects[monster_id].name + ), + LIGHT_GREEN, + ); + UseResult::UsedUp + } else { + // No enemy found within maximum range + game + .messages + .add("No enemy is close enough to strike.", RED); + UseResult::Cancelled + } +} + +// Find closest enemy, up to a maximum range, and in the player's FOV +fn closest_monster(tcod: &Tcod, objects: &[Object], max_range: i32) -> Option { + let mut closest_enemy = None; + // Start with (slightly more) than maximum range + let mut closest_dist = (max_range + 1) as f32; + + for (id, object) in objects.iter().enumerate() { + if (id != PLAYER) + && object.fighter.is_some() + && object.ai.is_some() + && tcod.fov.is_in_fov(object.x, object.y) + { + // Calculate the distance between this object and the player + let dist = objects[PLAYER].distance_to(object); + if dist < closest_dist { + // It's closer, so remember it + closest_enemy = Some(id); + closest_dist = dist; + } + } + } + closest_enemy +} + fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recompute: bool) { if fov_recompute { let player = &objects[PLAYER];