Mouse-look

This commit is contained in:
Michael Smith 2021-10-27 22:35:13 +02:00
parent 5b4f28a7c8
commit b3431cbdf9

View File

@ -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);
}
}
}