Targeting: the Fireball

This commit is contained in:
Michael Smith 2021-11-05 23:06:03 +01:00
parent 3a31fab361
commit 2640bea49e

View File

@ -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<Ai>,
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<Object>) -> PlayerAction {
@ -332,12 +357,15 @@ fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec<Object>) -> 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<Object>) {
// 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::<f32>();
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<Ai>,
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<T>(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<Object>) {
}
}
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<usize> {
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];