Targeting: the Fireball
This commit is contained in:
parent
3a31fab361
commit
2640bea49e
238
src/main.rs
238
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<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];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user