Mouse-look
This commit is contained in:
parent
5b4f28a7c8
commit
b3431cbdf9
187
src/main.rs
187
src/main.rs
@ -13,7 +13,7 @@ const SCREEN_WIDTH: i32 = 80;
|
||||
const SCREEN_HEIGHT: i32 = 50;
|
||||
const FPS: i32 = 20;
|
||||
const MAP_WIDTH: i32 = 80;
|
||||
const MAP_HEIGHT: i32 = 45;
|
||||
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,
|
||||
@ -38,10 +38,18 @@ 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;
|
||||
|
||||
struct Tcod {
|
||||
root: Root,
|
||||
con: Offscreen,
|
||||
panel: Offscreen,
|
||||
fov: FovMap,
|
||||
}
|
||||
|
||||
@ -94,7 +102,7 @@ impl Object {
|
||||
((dx.pow(2) + dy.pow(2)) as f32).sqrt()
|
||||
}
|
||||
|
||||
pub fn take_damage(&mut self, damage: i32) {
|
||||
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 {
|
||||
@ -106,26 +114,33 @@ impl Object {
|
||||
if let Some(fighter) = self.fighter {
|
||||
if fighter.hp <= 0 {
|
||||
self.alive = false;
|
||||
fighter.on_death.callback(self);
|
||||
fighter.on_death.callback(self, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attack(&mut self, target: &mut Object) {
|
||||
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
|
||||
println!(
|
||||
"{} attacks {} for {} hit points.",
|
||||
self.name, target.name, damage
|
||||
game.messages.add(
|
||||
format!(
|
||||
"{} attacks {} for {} hit points.",
|
||||
self.name, target.name, damage
|
||||
),
|
||||
WHITE,
|
||||
);
|
||||
target.take_damage(damage);
|
||||
|
||||
target.take_damage(damage, game);
|
||||
} else {
|
||||
println!(
|
||||
"{} attacks {} but it has no effect!",
|
||||
self.name, target.name
|
||||
)
|
||||
game.messages.add(
|
||||
format!(
|
||||
"{} attacks {} but it has no effect!",
|
||||
self.name, target.name
|
||||
),
|
||||
WHITE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,6 +174,7 @@ type Map = Vec<Vec<Tile>>;
|
||||
|
||||
struct Game {
|
||||
map: Map,
|
||||
messages: Messages,
|
||||
}
|
||||
|
||||
// A rectangle on the map, used to characterise a room.
|
||||
@ -201,6 +217,26 @@ struct Fighter {
|
||||
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,
|
||||
@ -220,17 +256,17 @@ enum DeathCallback {
|
||||
}
|
||||
|
||||
impl DeathCallback {
|
||||
fn callback(self, object: &mut Object) {
|
||||
fn callback(self, object: &mut Object, game: &mut Game) {
|
||||
use DeathCallback::*;
|
||||
let callback: fn(&mut Object) = match self {
|
||||
let callback = match self {
|
||||
Player => player_death,
|
||||
Monster => monster_death,
|
||||
};
|
||||
callback(object);
|
||||
callback(object, game);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_keys(tcod: &mut Tcod, game: &Game, objects: &mut Vec<Object>) -> PlayerAction {
|
||||
fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec<Object>) -> PlayerAction {
|
||||
use tcod::input::Key;
|
||||
use tcod::input::KeyCode::*;
|
||||
use PlayerAction::*;
|
||||
@ -400,7 +436,7 @@ fn move_by(id: usize, dx: i32, dy: i32, map: &Map, objects: &mut [Object]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn player_move_or_attack(dx: i32, dy: i32, game: &Game, objects: &mut [Object]) {
|
||||
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;
|
||||
@ -414,7 +450,7 @@ fn player_move_or_attack(dx: i32, dy: i32, game: &Game, objects: &mut [Object])
|
||||
match target_id {
|
||||
Some(target_id) => {
|
||||
let (player, target) = mut_two(PLAYER, target_id, objects);
|
||||
player.attack(target);
|
||||
player.attack(target, game);
|
||||
}
|
||||
None => {
|
||||
move_by(PLAYER, dx, dy, &game.map, objects);
|
||||
@ -435,7 +471,7 @@ fn move_towards(id: usize, target_x: i32, target_y: i32, map: &Map, objects: &mu
|
||||
move_by(id, dx, dy, map, objects);
|
||||
}
|
||||
|
||||
fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &Game, objects: &mut [Object]) {
|
||||
fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) {
|
||||
// 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) {
|
||||
@ -446,7 +482,7 @@ fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &Game, objects: &mut [Obje
|
||||
} 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);
|
||||
monster.attack(player, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -462,19 +498,21 @@ fn mut_two<T>(first_index: usize, second_index: usize, items: &mut [T]) -> (&mut
|
||||
}
|
||||
}
|
||||
|
||||
fn player_death(player: &mut Object) {
|
||||
fn player_death(player: &mut Object, game: &mut Game) {
|
||||
// The game ended!
|
||||
println!("You died!");
|
||||
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) {
|
||||
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
|
||||
println!("{} is dead!", monster.name);
|
||||
game
|
||||
.messages
|
||||
.add(format!("{} is dead!", monster.name), ORANGE);
|
||||
monster.char = '%';
|
||||
monster.color = DARK_RED;
|
||||
monster.blocks = false;
|
||||
@ -483,6 +521,41 @@ fn monster_death(monster: &mut Object) {
|
||||
monster.name = format!("remains of {}", monster.name);
|
||||
}
|
||||
|
||||
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 render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recompute: bool) {
|
||||
if fov_recompute {
|
||||
let player = &objects[PLAYER];
|
||||
@ -530,18 +603,6 @@ fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recomput
|
||||
}
|
||||
}
|
||||
|
||||
// Show the player's stats
|
||||
tcod.root.set_default_foreground(WHITE);
|
||||
if let Some(fighter) = objects[PLAYER].fighter {
|
||||
tcod.root.print_ex(
|
||||
1,
|
||||
SCREEN_HEIGHT - 2,
|
||||
BackgroundFlag::None,
|
||||
TextAlignment::Left,
|
||||
format!("HP: {}/{} ", fighter.hp, fighter.max_hp),
|
||||
);
|
||||
}
|
||||
|
||||
// Blit contents of "con" to the root console
|
||||
blit(
|
||||
&tcod.con,
|
||||
@ -552,6 +613,48 @@ fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recomput
|
||||
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,
|
||||
);
|
||||
|
||||
// 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 main() {
|
||||
@ -567,6 +670,7 @@ fn main() {
|
||||
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),
|
||||
};
|
||||
|
||||
@ -587,6 +691,7 @@ fn main() {
|
||||
let mut game = Game {
|
||||
// Generate map
|
||||
map: make_map(&mut objects),
|
||||
messages: Messages::new(),
|
||||
};
|
||||
|
||||
// Populate FOV map, according to generated map
|
||||
@ -603,6 +708,12 @@ fn main() {
|
||||
|
||||
let mut previous_player_position = (-1, -1);
|
||||
|
||||
// A warm welcoming message!
|
||||
game.messages.add(
|
||||
"Welcome stranger! Prepare to perish in the Tomb of Doom.",
|
||||
RED,
|
||||
);
|
||||
|
||||
// Main game loop
|
||||
while !tcod.root.window_closed() {
|
||||
tcod.con.clear();
|
||||
@ -612,7 +723,7 @@ fn main() {
|
||||
tcod.root.flush();
|
||||
|
||||
previous_player_position = objects[PLAYER].pos();
|
||||
let player_action = handle_keys(&mut tcod, &game, &mut objects);
|
||||
let player_action = handle_keys(&mut tcod, &mut game, &mut objects);
|
||||
if player_action == PlayerAction::Exit {
|
||||
break;
|
||||
}
|
||||
@ -621,7 +732,7 @@ fn main() {
|
||||
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, &mut objects);
|
||||
ai_take_turn(id, &tcod, &mut game, &mut objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user