1349 lines
35 KiB
Rust
1349 lines
35 KiB
Rust
#![allow(unused_imports)]
|
|
#![allow(unused_variables)]
|
|
#![allow(unused_mut)]
|
|
#![allow(dead_code)]
|
|
|
|
use rand::Rng;
|
|
use std::cmp;
|
|
use tcod::colors::*;
|
|
use tcod::console::*;
|
|
use tcod::input::{self, Event, Key, Mouse};
|
|
use tcod::map::{FovAlgorithm, Map as FovMap};
|
|
|
|
const SCREEN_WIDTH: i32 = 80;
|
|
const SCREEN_HEIGHT: i32 = 50;
|
|
const FPS: i32 = 20;
|
|
const MAP_WIDTH: i32 = 80;
|
|
const MAP_HEIGHT: i32 = 43;
|
|
const COLOR_DARK_WALL: Color = Color { r: 0, g: 0, b: 100 };
|
|
const COLOR_LIGHT_WALL: Color = Color {
|
|
r: 130,
|
|
g: 110,
|
|
b: 50,
|
|
};
|
|
const COLOR_DARK_GROUND: Color = Color {
|
|
r: 50,
|
|
g: 50,
|
|
b: 150,
|
|
};
|
|
const COLOR_LIGHT_GROUND: Color = Color {
|
|
r: 200,
|
|
g: 180,
|
|
b: 50,
|
|
};
|
|
const ROOM_MAX_SIZE: i32 = 10;
|
|
const ROOM_MIN_SIZE: i32 = 6;
|
|
const MAX_ROOMS: i32 = 30;
|
|
const FOV_ALGO: FovAlgorithm = FovAlgorithm::Basic;
|
|
const FOV_LIGHT_WALLS: bool = true;
|
|
const TORCH_RADIUS: i32 = 10;
|
|
const MAX_ROOM_MONSTERS: i32 = 3;
|
|
const PLAYER: usize = 0;
|
|
// Sizes and coordinates relevant for the GUI
|
|
const BAR_WIDTH: i32 = 20;
|
|
const PANEL_HEIGHT: i32 = 7;
|
|
const PANEL_Y: i32 = SCREEN_HEIGHT - PANEL_HEIGHT;
|
|
const MSG_X: i32 = BAR_WIDTH + 2;
|
|
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;
|
|
const FIREBALL_RADIUS: i32 = 3;
|
|
const FIREBALL_DAMAGE: i32 = 12;
|
|
|
|
struct Tcod {
|
|
root: Root,
|
|
con: Offscreen,
|
|
panel: Offscreen,
|
|
fov: FovMap,
|
|
key: Key,
|
|
mouse: Mouse,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Object {
|
|
x: i32,
|
|
y: i32,
|
|
char: char,
|
|
color: Color,
|
|
name: String,
|
|
blocks: bool,
|
|
alive: bool,
|
|
fighter: Option<Fighter>,
|
|
ai: Option<Ai>,
|
|
item: Option<Item>,
|
|
}
|
|
|
|
impl Object {
|
|
pub fn new(x: i32, y: i32, char: char, name: &str, color: Color, blocks: bool) -> Self {
|
|
Object {
|
|
x: x,
|
|
y: y,
|
|
char: char,
|
|
color: color,
|
|
name: name.into(),
|
|
blocks: blocks,
|
|
alive: false,
|
|
fighter: None,
|
|
ai: None,
|
|
item: None,
|
|
}
|
|
}
|
|
|
|
// Set the color and then draw the character that represents this object at its position
|
|
pub fn draw(&self, con: &mut dyn Console) {
|
|
con.set_default_foreground(self.color);
|
|
con.put_char(self.x, self.y, self.char, BackgroundFlag::None);
|
|
}
|
|
|
|
pub fn pos(&self) -> (i32, i32) {
|
|
(self.x, self.y)
|
|
}
|
|
|
|
pub fn set_pos(&mut self, x: i32, y: i32) {
|
|
self.x = x;
|
|
self.y = y;
|
|
}
|
|
|
|
pub fn distance_to(&self, other: &Object) -> f32 {
|
|
let dx = other.x - self.x;
|
|
let dy = other.y - self.y;
|
|
((dx.pow(2) + dy.pow(2)) as f32).sqrt()
|
|
}
|
|
|
|
pub fn take_damage(&mut self, damage: i32, game: &mut Game) {
|
|
// Apply damage if possible
|
|
if let Some(fighter) = self.fighter.as_mut() {
|
|
if damage > 0 {
|
|
fighter.hp -= damage;
|
|
}
|
|
}
|
|
|
|
// Check for death, call the death function
|
|
if let Some(fighter) = self.fighter {
|
|
if fighter.hp <= 0 {
|
|
self.alive = false;
|
|
fighter.on_death.callback(self, game);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn attack(&mut self, target: &mut Object, game: &mut Game) {
|
|
// A simple formula for attack damage
|
|
let damage = self.fighter.map_or(0, |f| f.power) - target.fighter.map_or(0, |f| f.defense);
|
|
if damage > 0 {
|
|
// Make the target take some damage
|
|
game.messages.add(
|
|
format!(
|
|
"{} attacks {} for {} hit points.",
|
|
self.name, target.name, damage
|
|
),
|
|
WHITE,
|
|
);
|
|
|
|
target.take_damage(damage, game);
|
|
} else {
|
|
game.messages.add(
|
|
format!(
|
|
"{} attacks {} but it has no effect!",
|
|
self.name, target.name
|
|
),
|
|
WHITE,
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the distance to some coordinates
|
|
pub fn distance(&self, x: i32, y: i32) -> f32 {
|
|
(((x - self.x).pow(2) + (y - self.y).pow(2)) as f32).sqrt()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct Tile {
|
|
blocked: bool,
|
|
explored: bool,
|
|
block_sight: bool,
|
|
}
|
|
|
|
impl Tile {
|
|
pub fn empty() -> Self {
|
|
Tile {
|
|
blocked: false,
|
|
explored: false,
|
|
block_sight: false,
|
|
}
|
|
}
|
|
|
|
pub fn wall() -> Self {
|
|
Tile {
|
|
blocked: true,
|
|
explored: false,
|
|
block_sight: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
type Map = Vec<Vec<Tile>>;
|
|
|
|
struct Game {
|
|
map: Map,
|
|
messages: Messages,
|
|
inventory: Vec<Object>,
|
|
}
|
|
|
|
// A rectangle on the map, used to characterise a room.
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct Rect {
|
|
x1: i32,
|
|
y1: i32,
|
|
x2: i32,
|
|
y2: i32,
|
|
}
|
|
|
|
impl Rect {
|
|
pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
|
|
Rect {
|
|
x1: x,
|
|
y1: y,
|
|
x2: x + w,
|
|
y2: y + h,
|
|
}
|
|
}
|
|
|
|
pub fn center(&self) -> (i32, i32) {
|
|
let center_x = (self.x1 + self.x2) / 2;
|
|
let center_y = (self.y1 + self.y2) / 2;
|
|
(center_x, center_y)
|
|
}
|
|
|
|
pub fn intersects_with(&self, other: &Rect) -> bool {
|
|
(self.x1 <= other.x2) && (self.x2 >= other.x1) && (self.y1 <= other.y2) && (self.y2 >= other.y1)
|
|
}
|
|
}
|
|
|
|
// Combat related properties and methods (monster, player, NPC).
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
struct Fighter {
|
|
max_hp: i32,
|
|
hp: i32,
|
|
defense: i32,
|
|
power: i32,
|
|
on_death: DeathCallback,
|
|
}
|
|
|
|
struct Messages {
|
|
messages: Vec<(String, Color)>,
|
|
}
|
|
|
|
impl Messages {
|
|
pub fn new() -> Self {
|
|
Self { messages: vec![] }
|
|
}
|
|
|
|
// Add the new message as a tuple, with the text and the color
|
|
pub fn add<T: Into<String>>(&mut self, message: T, color: Color) {
|
|
self.messages.push((message.into(), color));
|
|
}
|
|
|
|
// Create a `DoubleEndedIterator` over the messages
|
|
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &(String, Color)> {
|
|
self.messages.iter()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
enum PlayerAction {
|
|
TookTurn,
|
|
DidntTakeTurn,
|
|
Exit,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum Ai {
|
|
Basic,
|
|
Confused {
|
|
previous_ai: Box<Ai>,
|
|
num_turns: i32,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
enum DeathCallback {
|
|
Player,
|
|
Monster,
|
|
}
|
|
|
|
impl DeathCallback {
|
|
fn callback(self, object: &mut Object, game: &mut Game) {
|
|
use DeathCallback::*;
|
|
let callback = match self {
|
|
Player => player_death,
|
|
Monster => monster_death,
|
|
};
|
|
callback(object, game);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
enum Item {
|
|
Heal,
|
|
Lightning,
|
|
Confuse,
|
|
Fireball,
|
|
}
|
|
|
|
enum UseResult {
|
|
UsedUp,
|
|
Cancelled,
|
|
}
|
|
|
|
fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec<Object>) -> PlayerAction {
|
|
use tcod::input::Key;
|
|
use tcod::input::KeyCode::*;
|
|
use PlayerAction::*;
|
|
|
|
let player_alive = objects[PLAYER].alive;
|
|
match (tcod.key, tcod.key.text(), player_alive) {
|
|
(
|
|
Key {
|
|
code: Enter,
|
|
alt: true,
|
|
..
|
|
},
|
|
_,
|
|
_,
|
|
) => {
|
|
// Alt+Enter: toggle fullscreen
|
|
let fullscreen = tcod.root.is_fullscreen();
|
|
tcod.root.set_fullscreen(!fullscreen);
|
|
DidntTakeTurn
|
|
}
|
|
(Key { code: Escape, .. }, _, _) => Exit, // exit game
|
|
|
|
(Key { code: Up, .. }, _, true) => {
|
|
player_move_or_attack(0, -1, game, objects);
|
|
TookTurn
|
|
}
|
|
(Key { code: Down, .. }, _, true) => {
|
|
player_move_or_attack(0, 1, game, objects);
|
|
TookTurn
|
|
}
|
|
(Key { code: Left, .. }, _, true) => {
|
|
player_move_or_attack(-1, 0, game, objects);
|
|
TookTurn
|
|
}
|
|
(Key { code: Right, .. }, _, true) => {
|
|
player_move_or_attack(1, 0, game, objects);
|
|
TookTurn
|
|
}
|
|
|
|
(Key { code: Text, .. }, "g", true) => {
|
|
// Pick up an item
|
|
let item_id = objects
|
|
.iter()
|
|
.position(|object| object.pos() == objects[PLAYER].pos() && object.item.is_some());
|
|
if let Some(item_id) = item_id {
|
|
pick_item_up(item_id, game, objects);
|
|
}
|
|
DidntTakeTurn
|
|
}
|
|
|
|
(Key { code: Text, .. }, "i", true) => {
|
|
// Show the inventory
|
|
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,
|
|
);
|
|
if let Some(inventory_index) = inventory_index {
|
|
use_item(inventory_index, tcod, game, objects);
|
|
}
|
|
DidntTakeTurn
|
|
}
|
|
|
|
(Key { code: Text, .. }, "d", true) => {
|
|
// Show the inventory; if an item is selected, drop it
|
|
let inventory_index = inventory_menu(
|
|
&game.inventory,
|
|
"Press the key next to an item to drop it, or any other to cancel.\n",
|
|
&mut tcod.root,
|
|
);
|
|
if let Some(inventory_index) = inventory_index {
|
|
drop_item(inventory_index, game, objects);
|
|
}
|
|
DidntTakeTurn
|
|
}
|
|
|
|
_ => DidntTakeTurn,
|
|
}
|
|
}
|
|
|
|
fn make_map(objects: &mut Vec<Object>) -> Map {
|
|
// Fill the map with unblocked tiles
|
|
let mut map = vec![vec![Tile::wall(); MAP_HEIGHT as usize]; MAP_WIDTH as usize];
|
|
|
|
let mut rooms = vec![];
|
|
|
|
for _ in 0..MAX_ROOMS {
|
|
let w = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1);
|
|
let h = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1);
|
|
let x = rand::thread_rng().gen_range(0, MAP_WIDTH - w);
|
|
let y = rand::thread_rng().gen_range(0, MAP_HEIGHT - h);
|
|
|
|
let new_room = Rect::new(x, y, w, h);
|
|
let failed = rooms
|
|
.iter()
|
|
.any(|other_room| new_room.intersects_with(other_room));
|
|
|
|
if !failed {
|
|
create_room(new_room, &mut map);
|
|
place_objects(new_room, &map, objects);
|
|
let (new_x, new_y) = new_room.center();
|
|
|
|
if rooms.is_empty() {
|
|
objects[PLAYER].set_pos(new_x, new_y);
|
|
} else {
|
|
let (prev_x, prev_y) = rooms[rooms.len() - 1].center();
|
|
|
|
if rand::random() {
|
|
create_h_tunnel(prev_x, new_x, prev_y, &mut map);
|
|
create_v_tunnel(prev_y, new_y, new_x, &mut map);
|
|
} else {
|
|
create_v_tunnel(prev_y, new_y, prev_x, &mut map);
|
|
create_h_tunnel(prev_x, new_x, new_y, &mut map);
|
|
}
|
|
}
|
|
rooms.push(new_room);
|
|
}
|
|
}
|
|
|
|
map
|
|
}
|
|
|
|
fn create_room(room: Rect, map: &mut Map) {
|
|
// Go through the tiles in the rectangle and make them passable
|
|
for x in (room.x1 + 1)..room.x2 {
|
|
for y in (room.y1 + 1)..room.y2 {
|
|
map[x as usize][y as usize] = Tile::empty();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn create_h_tunnel(x1: i32, x2: i32, y: i32, map: &mut Map) {
|
|
// Horizontal tunnel. `min()` and `max()` are used in case
|
|
// `x1 > x2`
|
|
for x in cmp::min(x1, x2)..(cmp::max(x1, x2) + 1) {
|
|
map[x as usize][y as usize] = Tile::empty();
|
|
}
|
|
}
|
|
|
|
fn create_v_tunnel(y1: i32, y2: i32, x: i32, map: &mut Map) {
|
|
// Vertical tunnel
|
|
for y in cmp::min(y1, y2)..(cmp::max(y1, y2) + 1) {
|
|
map[x as usize][y as usize] = Tile::empty();
|
|
}
|
|
}
|
|
|
|
fn place_objects(room: Rect, map: &Map, objects: &mut Vec<Object>) {
|
|
// Generate some monsters
|
|
let num_monsters = rand::thread_rng().gen_range(0, MAX_ROOM_MONSTERS + 1);
|
|
|
|
for _ in 0..num_monsters {
|
|
let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2);
|
|
let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2);
|
|
|
|
if !is_blocked(x, y, map, objects) {
|
|
let mut monster = if rand::random::<f32>() < 0.8 {
|
|
// 80% chance of spawning an orc
|
|
let mut orc = Object::new(x, y, 'o', "orc", DESATURATED_GREEN, true);
|
|
orc.fighter = Some(Fighter {
|
|
max_hp: 10,
|
|
hp: 10,
|
|
defense: 0,
|
|
power: 3,
|
|
on_death: DeathCallback::Monster,
|
|
});
|
|
orc.ai = Some(Ai::Basic);
|
|
orc
|
|
} else {
|
|
// 20% chance of spawning a troll
|
|
let mut troll = Object::new(x, y, 'T', "troll", DARKER_GREEN, true);
|
|
troll.fighter = Some(Fighter {
|
|
max_hp: 16,
|
|
hp: 16,
|
|
defense: 1,
|
|
power: 4,
|
|
on_death: DeathCallback::Monster,
|
|
});
|
|
troll.ai = Some(Ai::Basic);
|
|
troll
|
|
};
|
|
|
|
monster.alive = true;
|
|
objects.push(monster);
|
|
}
|
|
}
|
|
|
|
// Generate some items
|
|
let num_items = rand::thread_rng().gen_range(0, MAX_ROOM_ITEMS + 1);
|
|
|
|
for _ in 0..num_items {
|
|
// Choose random spot for this item
|
|
let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2);
|
|
let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2);
|
|
|
|
// Only place it if the tile is note blocked
|
|
if !is_blocked(x, y, map, objects) {
|
|
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 if dice < 0.7 + 0.1 + 0.1 {
|
|
// Create a fireball scroll (10% chance)
|
|
let mut object = Object::new(x, y, '#', "scroll of fireball", LIGHT_YELLOW, false);
|
|
object.item = Some(Item::Fireball);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_blocked(x: i32, y: i32, map: &Map, objects: &[Object]) -> bool {
|
|
if map[x as usize][y as usize].blocked {
|
|
return true;
|
|
}
|
|
|
|
objects
|
|
.iter()
|
|
.any(|object| object.blocks && object.pos() == (x, y))
|
|
}
|
|
|
|
// Move by given amount, if the destination is not blocked
|
|
fn move_by(id: usize, dx: i32, dy: i32, map: &Map, objects: &mut [Object]) {
|
|
let (x, y) = objects[id].pos();
|
|
if !is_blocked(x + dx, y + dy, map, objects) {
|
|
objects[id].set_pos(x + dx, y + dy);
|
|
}
|
|
}
|
|
|
|
fn player_move_or_attack(dx: i32, dy: i32, game: &mut Game, objects: &mut [Object]) {
|
|
// The coordinates the player is moving to / attacking
|
|
let x = objects[PLAYER].x + dx;
|
|
let y = objects[PLAYER].y + dy;
|
|
|
|
// Try to find an attackable object there
|
|
let target_id = objects
|
|
.iter()
|
|
.position(|object| object.fighter.is_some() && object.pos() == (x, y));
|
|
|
|
// Attack target if found, move otherwise
|
|
match target_id {
|
|
Some(target_id) => {
|
|
let (player, target) = mut_two(PLAYER, target_id, objects);
|
|
player.attack(target, game);
|
|
}
|
|
None => {
|
|
move_by(PLAYER, dx, dy, &game.map, objects);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn move_towards(id: usize, target_x: i32, target_y: i32, map: &Map, objects: &mut [Object]) {
|
|
// Vector from this object to the target, and distance
|
|
let dx = target_x - objects[id].x;
|
|
let dy = target_y - objects[id].y;
|
|
let distance = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();
|
|
|
|
// Normalize it to length 1 (preserving direction), then round it and
|
|
// convert it to integer so the movement is restricted to the map grid
|
|
let dx = (dx as f32 / distance).round() as i32;
|
|
let dy = (dy as f32 / distance).round() as i32;
|
|
move_by(id, dx, dy, map, objects);
|
|
}
|
|
|
|
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) {
|
|
if objects[monster_id].distance_to(&objects[PLAYER]) >= 2.0 {
|
|
// Move towards player if far away
|
|
let (player_x, player_y) = objects[PLAYER].pos();
|
|
move_towards(monster_id, player_x, player_y, &game.map, objects);
|
|
} else if objects[PLAYER].fighter.map_or(false, |f| f.hp > 0) {
|
|
// Close enough, attach! (if the player is still alive.)
|
|
let (monster, player) = mut_two(monster_id, PLAYER, objects);
|
|
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) {
|
|
assert!(first_index != second_index);
|
|
let split_at_index = cmp::max(first_index, second_index);
|
|
let (first_slice, second_slice) = items.split_at_mut(split_at_index);
|
|
if first_index < second_index {
|
|
(&mut first_slice[first_index], &mut second_slice[0])
|
|
} else {
|
|
(&mut second_slice[0], &mut first_slice[second_index])
|
|
}
|
|
}
|
|
|
|
fn player_death(player: &mut Object, game: &mut Game) {
|
|
// The game ended!
|
|
game.messages.add("You died!", RED);
|
|
|
|
// For added effect, transform the player into corpse!
|
|
player.char = '%';
|
|
player.color = DARK_RED;
|
|
}
|
|
|
|
fn monster_death(monster: &mut Object, game: &mut Game) {
|
|
// Transform it into a nasty corpse! It doesn't block, can't be
|
|
// attacked and doesn't move
|
|
game
|
|
.messages
|
|
.add(format!("{} is dead!", monster.name), ORANGE);
|
|
monster.char = '%';
|
|
monster.color = DARK_RED;
|
|
monster.blocks = false;
|
|
monster.fighter = None;
|
|
monster.ai = None;
|
|
monster.name = format!("remains of {}", monster.name);
|
|
}
|
|
|
|
fn menu<T: AsRef<str>>(header: &str, options: &[T], width: i32, root: &mut Root) -> Option<usize> {
|
|
// Limit the amount of menu options
|
|
assert!(
|
|
options.len() <= 26,
|
|
"Cannot have a menu with more than 26 options."
|
|
);
|
|
|
|
// Calculate total height for the header (after auto-wrap) and one line per option
|
|
let header_height = if header.is_empty() {
|
|
0
|
|
} else {
|
|
root.get_height_rect(0, 0, width, SCREEN_HEIGHT, header)
|
|
};
|
|
let height = options.len() as i32 + header_height;
|
|
|
|
// Create an off-screen console that represents the menu's window
|
|
let mut window = Offscreen::new(width, height);
|
|
|
|
// Print the header, with auto-wrap
|
|
window.set_default_foreground(WHITE);
|
|
window.print_rect_ex(
|
|
0,
|
|
0,
|
|
width,
|
|
height,
|
|
BackgroundFlag::None,
|
|
TextAlignment::Left,
|
|
header,
|
|
);
|
|
|
|
// Print all the options
|
|
for (index, option_text) in options.iter().enumerate() {
|
|
let menu_letter = (b'a' + index as u8) as char;
|
|
let text = format!("({}) {}", menu_letter, option_text.as_ref());
|
|
window.print_ex(
|
|
0,
|
|
header_height + index as i32,
|
|
BackgroundFlag::None,
|
|
TextAlignment::Left,
|
|
text,
|
|
);
|
|
}
|
|
|
|
// Blit the contents of the menu window to the root console
|
|
let x = SCREEN_WIDTH / 2 - width / 2;
|
|
let y = SCREEN_HEIGHT / 2 - height / 2;
|
|
blit(&window, (0, 0), (width, height), root, (x, y), 1.0, 0.7);
|
|
|
|
// Present the root console to the player and wait for a key-press
|
|
root.flush();
|
|
let key = root.wait_for_keypress(true);
|
|
|
|
// Convert the ASCII code to an index; if it corresponds to an option, return it
|
|
if key.printable.is_alphabetic() {
|
|
let index = key.printable.to_ascii_lowercase() as usize - 'a' as usize;
|
|
if index < options.len() {
|
|
Some(index)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn inventory_menu(inventory: &[Object], header: &str, root: &mut Root) -> Option<usize> {
|
|
// Show a menu with each inventory item as an option
|
|
let options = if inventory.len() == 0 {
|
|
vec!["Inventory is empty.".into()]
|
|
} else {
|
|
inventory.iter().map(|item| item.name.clone()).collect()
|
|
};
|
|
|
|
let inventory_index = menu(header, &options, INVENTORY_WIDTH, root);
|
|
|
|
// If an item was chosen, return it
|
|
if inventory.len() > 0 {
|
|
inventory_index
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn render_bar(
|
|
panel: &mut Offscreen,
|
|
x: i32,
|
|
y: i32,
|
|
total_width: i32,
|
|
name: &str,
|
|
value: i32,
|
|
maximum: i32,
|
|
bar_color: Color,
|
|
back_color: Color,
|
|
) {
|
|
// Render a bar (HP, experience, etc). First calculate the width of the bar
|
|
let bar_width = (value as f32 / maximum as f32 * total_width as f32) as i32;
|
|
|
|
// Render the background first
|
|
panel.set_default_background(back_color);
|
|
panel.rect(x, y, total_width, 1, false, BackgroundFlag::Screen);
|
|
|
|
// Now render the bar on top
|
|
panel.set_default_background(bar_color);
|
|
if bar_width > 0 {
|
|
panel.rect(x, y, bar_width, 1, false, BackgroundFlag::Screen);
|
|
}
|
|
|
|
// Finally, some centered text with the values
|
|
panel.set_default_foreground(WHITE);
|
|
panel.print_ex(
|
|
x + total_width / 2,
|
|
y,
|
|
BackgroundFlag::None,
|
|
TextAlignment::Center,
|
|
&format!("{}: {}/{}", name, value, maximum),
|
|
);
|
|
}
|
|
|
|
fn get_names_under_mouse(mouse: Mouse, objects: &[Object], fov_map: &FovMap) -> String {
|
|
let (x, y) = (mouse.cx as i32, mouse.cy as i32);
|
|
|
|
// Create a list with the names of all objects at the mouse's coordinates and in FOV
|
|
let names = objects
|
|
.iter()
|
|
.filter(|obj| obj.pos() == (x, y) && fov_map.is_in_fov(obj.x, obj.y))
|
|
.map(|obj| obj.name.clone())
|
|
.collect::<Vec<_>>();
|
|
|
|
names.join(", ")
|
|
}
|
|
|
|
fn pick_item_up(object_id: usize, game: &mut Game, objects: &mut Vec<Object>) {
|
|
if game.inventory.len() >= 26 {
|
|
game.messages.add(
|
|
format!(
|
|
"Your inventory is full, cannot pick up {}.",
|
|
objects[object_id].name
|
|
),
|
|
RED,
|
|
);
|
|
} else {
|
|
let item = objects.swap_remove(object_id);
|
|
game
|
|
.messages
|
|
.add(format!("You picked up a {}!", item.name), GREEN);
|
|
game.inventory.push(item);
|
|
}
|
|
}
|
|
|
|
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,
|
|
Fireball => cast_fireball,
|
|
};
|
|
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 {
|
|
game.messages.add(
|
|
"Left-click an enemy to confuse it, or right click to cancel.",
|
|
LIGHT_CYAN,
|
|
);
|
|
let monster_id = target_monster(tcod, game, objects, Some(CONFUSE_RANGE as f32));
|
|
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
|
|
}
|
|
}
|
|
|
|
fn cast_fireball(
|
|
_inventory_id: usize,
|
|
tcod: &mut Tcod,
|
|
game: &mut Game,
|
|
objects: &mut [Object],
|
|
) -> UseResult {
|
|
// Ask the player for a target tile to throw a fireball at
|
|
game.messages.add(
|
|
"Left-click a target tile for the fireball, or right-click to cancel.",
|
|
LIGHT_CYAN,
|
|
);
|
|
let (x, y) = match target_tile(tcod, game, objects, None) {
|
|
Some(tile_pos) => tile_pos,
|
|
None => return UseResult::Cancelled,
|
|
};
|
|
game.messages.add(
|
|
format!(
|
|
"The fireball explodes, burning everything within {} tiles!",
|
|
FIREBALL_RADIUS
|
|
),
|
|
ORANGE,
|
|
);
|
|
|
|
for obj in objects {
|
|
if obj.distance(x, y) <= FIREBALL_RADIUS as f32 && obj.fighter.is_some() {
|
|
game.messages.add(
|
|
format!(
|
|
"The {} gets burned for {} hit points.",
|
|
obj.name, FIREBALL_DAMAGE
|
|
),
|
|
ORANGE,
|
|
);
|
|
obj.take_damage(FIREBALL_DAMAGE, game);
|
|
}
|
|
}
|
|
UseResult::UsedUp
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Return the position of a tile left-clicked in player's FOV (optionally in a
|
|
// range), or (None, None) if right-clicked.
|
|
fn target_tile(
|
|
tcod: &mut Tcod,
|
|
game: &mut Game,
|
|
objects: &[Object],
|
|
max_range: Option<f32>,
|
|
) -> Option<(i32, i32)> {
|
|
use tcod::input::KeyCode::Escape;
|
|
|
|
loop {
|
|
// Render the screen. This erases the inventory and shows the names of
|
|
// objects under the mouse.
|
|
tcod.root.flush();
|
|
let event = input::check_for_event(input::KEY_PRESS | input::MOUSE).map(|e| e.1);
|
|
match event {
|
|
Some(Event::Mouse(m)) => tcod.mouse = m,
|
|
Some(Event::Key(k)) => tcod.key = k,
|
|
None => tcod.key = Default::default(),
|
|
}
|
|
render_all(tcod, game, objects, false);
|
|
|
|
let (x, y) = (tcod.mouse.cx as i32, tcod.mouse.cy as i32);
|
|
|
|
// Accept the target if the player clicked in FOV, and in case a range
|
|
// is specified, if it's in that range
|
|
let in_fov = (x < MAP_WIDTH) && (y < MAP_HEIGHT) && tcod.fov.is_in_fov(x, y);
|
|
let in_range = max_range.map_or(true, |range| objects[PLAYER].distance(x, y) <= range);
|
|
if tcod.mouse.lbutton_pressed && in_fov && in_range {
|
|
return Some((x, y));
|
|
}
|
|
|
|
if tcod.mouse.rbutton_pressed || tcod.key.code == Escape {
|
|
return None; // Cancel if the player right-clicked or pressed escape.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns a clicked monster inside FOV up to a range, or None if right-clicked
|
|
fn target_monster(
|
|
tcod: &mut Tcod,
|
|
game: &mut Game,
|
|
objects: &[Object],
|
|
max_range: Option<f32>,
|
|
) -> Option<usize> {
|
|
loop {
|
|
match target_tile(tcod, game, objects, max_range) {
|
|
Some((x, y)) => {
|
|
// Return the first clicked monster, otherwise continue looping
|
|
for (id, obj) in objects.iter().enumerate() {
|
|
if obj.pos() == (x, y) && obj.fighter.is_some() && id != PLAYER {
|
|
return Some(id);
|
|
}
|
|
}
|
|
}
|
|
None => return None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn drop_item(inventory_id: usize, game: &mut Game, objects: &mut Vec<Object>) {
|
|
let mut item = game.inventory.remove(inventory_id);
|
|
item.set_pos(objects[PLAYER].x, objects[PLAYER].y);
|
|
game
|
|
.messages
|
|
.add(format!("You dropped a {}.", item.name), YELLOW);
|
|
objects.push(item);
|
|
}
|
|
|
|
fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recompute: bool) {
|
|
if fov_recompute {
|
|
let player = &objects[PLAYER];
|
|
tcod
|
|
.fov
|
|
.compute_fov(player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO);
|
|
}
|
|
|
|
// Only render objects in fov
|
|
let mut to_draw: Vec<_> = objects
|
|
.iter()
|
|
.filter(|o| tcod.fov.is_in_fov(o.x, o.y))
|
|
.collect();
|
|
|
|
// Sort so that non-blocking objects are rendered behind blocking objects
|
|
to_draw.sort_by(|o1, o2| o1.blocks.cmp(&o2.blocks));
|
|
|
|
// Draw objects in the list
|
|
for object in &to_draw {
|
|
object.draw(&mut tcod.con);
|
|
}
|
|
|
|
// Go through all tiles and set their background color
|
|
for y in 0..MAP_HEIGHT {
|
|
for x in 0..MAP_WIDTH {
|
|
let visible = tcod.fov.is_in_fov(x, y);
|
|
let wall = game.map[x as usize][y as usize].block_sight;
|
|
let color = match (visible, wall) {
|
|
// Outside fov:
|
|
(false, true) => COLOR_DARK_WALL,
|
|
(false, false) => COLOR_DARK_GROUND,
|
|
// INside fov:
|
|
(true, true) => COLOR_LIGHT_WALL,
|
|
(true, false) => COLOR_LIGHT_GROUND,
|
|
};
|
|
let explored = &mut game.map[x as usize][y as usize].explored;
|
|
if visible {
|
|
*explored = true;
|
|
}
|
|
if *explored {
|
|
tcod
|
|
.con
|
|
.set_char_background(x, y, color, BackgroundFlag::Set);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Blit contents of "con" to the root console
|
|
blit(
|
|
&tcod.con,
|
|
(0, 0),
|
|
(MAP_WIDTH, MAP_HEIGHT),
|
|
&mut tcod.root,
|
|
(0, 0),
|
|
1.0,
|
|
1.0,
|
|
);
|
|
|
|
// Prepare to render the GUI panel
|
|
tcod.panel.set_default_background(BLACK);
|
|
tcod.panel.clear();
|
|
|
|
// Show the player's stats
|
|
let hp = objects[PLAYER].fighter.map_or(0, |f| f.hp);
|
|
let max_hp = objects[PLAYER].fighter.map_or(0, |f| f.max_hp);
|
|
render_bar(
|
|
&mut tcod.panel,
|
|
1,
|
|
1,
|
|
BAR_WIDTH,
|
|
"HP",
|
|
hp,
|
|
max_hp,
|
|
LIGHT_RED,
|
|
DARKER_RED,
|
|
);
|
|
|
|
// Display names of objects under the mouse
|
|
tcod.panel.set_default_foreground(LIGHT_GREY);
|
|
tcod.panel.print_ex(
|
|
1,
|
|
0,
|
|
BackgroundFlag::None,
|
|
TextAlignment::Left,
|
|
get_names_under_mouse(tcod.mouse, objects, &tcod.fov),
|
|
);
|
|
|
|
// Print the game messages, one line at a time
|
|
let mut y = MSG_HEIGHT as i32;
|
|
for &(ref msg, color) in game.messages.iter().rev() {
|
|
let msg_height = tcod.panel.get_height_rect(MSG_X, y, MSG_WIDTH, 0, msg);
|
|
y -= msg_height;
|
|
if y < 0 {
|
|
break;
|
|
}
|
|
tcod.panel.set_default_foreground(color);
|
|
tcod.panel.print_rect(MSG_X, y, MSG_WIDTH, 0, msg);
|
|
}
|
|
|
|
// Blit contents of "panel" to the root console
|
|
blit(
|
|
&tcod.panel,
|
|
(0, 0),
|
|
(SCREEN_WIDTH, PANEL_HEIGHT),
|
|
&mut tcod.root,
|
|
(0, PANEL_Y),
|
|
1.0,
|
|
1.0,
|
|
);
|
|
}
|
|
|
|
fn initialise_fov(tcod: &mut Tcod, map: &Map) {
|
|
// Populate FOV map, according to generated map
|
|
for y in 0..MAP_HEIGHT {
|
|
for x in 0..MAP_WIDTH {
|
|
tcod.fov.set(
|
|
x,
|
|
y,
|
|
!map[x as usize][y as usize].block_sight,
|
|
!map[x as usize][y as usize].blocked,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn new_game(tcod: &mut Tcod) -> (Game, Vec<Object>) {
|
|
// Create player object
|
|
let mut player = Object::new(0, 0, '@', "player", WHITE, true);
|
|
player.alive = true;
|
|
player.fighter = Some(Fighter {
|
|
max_hp: 30,
|
|
hp: 30,
|
|
defense: 2,
|
|
power: 5,
|
|
on_death: DeathCallback::Player,
|
|
});
|
|
|
|
// List of objects in the world
|
|
let mut objects = vec![player];
|
|
|
|
let mut game = Game {
|
|
// Generate map
|
|
map: make_map(&mut objects),
|
|
messages: Messages::new(),
|
|
inventory: vec![],
|
|
};
|
|
|
|
initialise_fov(tcod, &game.map);
|
|
|
|
// A warm welcoming message!
|
|
game.messages.add(
|
|
"Welcome stranger! Prepare to perish in the Tomb of Doom.",
|
|
RED,
|
|
);
|
|
|
|
(game, objects)
|
|
}
|
|
|
|
fn play_game(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec<Object>) {
|
|
// force FOV "recompute" first time through the game loop
|
|
let mut previous_player_position = (-1, -1);
|
|
|
|
// Main game loop
|
|
while !tcod.root.window_closed() {
|
|
// clear the screen of the previous frame
|
|
tcod.con.clear();
|
|
|
|
match input::check_for_event(input::MOUSE | input::KEY_PRESS) {
|
|
Some((_, Event::Mouse(m))) => tcod.mouse = m,
|
|
Some((_, Event::Key(k))) => tcod.key = k,
|
|
_ => tcod.key = Default::default(),
|
|
}
|
|
|
|
// Render the screen
|
|
let fov_recompute = previous_player_position != (objects[PLAYER].pos());
|
|
render_all(tcod, game, &objects, fov_recompute);
|
|
|
|
tcod.root.flush();
|
|
|
|
// Handle keys and exit game if needed
|
|
previous_player_position = objects[PLAYER].pos();
|
|
let player_action = handle_keys(tcod, game, objects);
|
|
if player_action == PlayerAction::Exit {
|
|
break;
|
|
}
|
|
|
|
// Let monsters take their turn
|
|
if objects[PLAYER].alive && player_action != PlayerAction::DidntTakeTurn {
|
|
for id in 0..objects.len() {
|
|
if objects[id].ai.is_some() {
|
|
ai_take_turn(id, tcod, game, objects);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main_menu(tcod: &mut Tcod) {
|
|
let img = tcod::image::Image::from_file("menu_background.png")
|
|
.ok()
|
|
.expect("Background image not found");
|
|
|
|
while !tcod.root.window_closed() {
|
|
// Show the background image, at twice the regular console resolution
|
|
tcod::image::blit_2x(&img, (0, 0), (-1, -1), &mut tcod.root, (0, 0));
|
|
|
|
tcod.root.set_default_foreground(LIGHT_YELLOW);
|
|
tcod.root.print_ex(
|
|
SCREEN_WIDTH / 2,
|
|
SCREEN_HEIGHT / 2 - 4,
|
|
BackgroundFlag::None,
|
|
TextAlignment::Center,
|
|
"TOMB OF DOOM",
|
|
);
|
|
tcod.root.print_ex(
|
|
SCREEN_WIDTH / 2,
|
|
SCREEN_HEIGHT - 2,
|
|
BackgroundFlag::None,
|
|
TextAlignment::Center,
|
|
"By m",
|
|
);
|
|
|
|
// Show options and wait for the player's choice
|
|
let choices = &["Play a new game", "Continue last game", "Quit"];
|
|
let choice = menu("", choices, 24, &mut tcod.root);
|
|
|
|
match choice {
|
|
Some(0) => {
|
|
// New game
|
|
let (mut game, mut objects) = new_game(tcod);
|
|
play_game(tcod, &mut game, &mut objects);
|
|
}
|
|
Some(2) => {
|
|
// Quit
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
tcod::system::set_fps(FPS);
|
|
|
|
let root = Root::initializer()
|
|
.font("arial10x10.png", FontLayout::Tcod)
|
|
.font_type(FontType::Greyscale)
|
|
.size(SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
.title("Rust/libtcod tutorial")
|
|
.init();
|
|
|
|
let mut tcod = Tcod {
|
|
root,
|
|
con: Offscreen::new(MAP_WIDTH, MAP_HEIGHT),
|
|
panel: Offscreen::new(SCREEN_WIDTH, PANEL_HEIGHT),
|
|
fov: FovMap::new(MAP_WIDTH, MAP_HEIGHT),
|
|
key: Default::default(),
|
|
mouse: Default::default(),
|
|
};
|
|
|
|
main_menu(&mut tcod);
|
|
}
|