From 3211619b96e4beb7f0a31fba2eb4ade8ceac1d31 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 17 Nov 2021 22:59:16 +0100 Subject: [PATCH] Monster and item progression. --- src/main.rs | 127 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index dd598b8..b9bfc22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,11 @@ const CONFUSE_RANGE: i32 = 8; const CONFUSE_NUM_TURNS: i32 = 10; const FIREBALL_RADIUS: i32 = 3; const FIREBALL_DAMAGE: i32 = 12; +// Experience and level-ups +const LEVEL_UP_BASE: i32 = 200; +const LEVEL_UP_FACTOR: i32 = 150; +const LEVEL_SCREEN_WIDTH: i32 = 40; +const CHARACTER_SCREEN_WIDTH: i32 = 30; struct Tcod { root: Root, @@ -82,6 +87,7 @@ struct Object { ai: Option, item: Option, always_visible: bool, + level: i32, } impl Object { @@ -98,6 +104,7 @@ impl Object { ai: None, item: None, always_visible: false, + level: 1, } } @@ -122,7 +129,7 @@ impl Object { ((dx.pow(2) + dy.pow(2)) as f32).sqrt() } - pub fn take_damage(&mut self, damage: i32, game: &mut Game) { + pub fn take_damage(&mut self, damage: i32, game: &mut Game) -> Option { // Apply damage if possible if let Some(fighter) = self.fighter.as_mut() { if damage > 0 { @@ -135,8 +142,10 @@ impl Object { if fighter.hp <= 0 { self.alive = false; fighter.on_death.callback(self, game); + return Some(fighter.xp); } } + None } pub fn attack(&mut self, target: &mut Object, game: &mut Game) { @@ -152,7 +161,10 @@ impl Object { WHITE, ); - target.take_damage(damage, game); + if let Some(xp) = target.take_damage(damage, game) { + // Yield experience to the player + self.fighter.as_mut().unwrap().xp += xp; + } } else { game.messages.add( format!( @@ -251,6 +263,7 @@ struct Fighter { hp: i32, defense: i32, power: i32, + xp: i32, on_death: DeathCallback, } @@ -343,6 +356,11 @@ fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) -> P } (Key { code: Escape, .. }, _, _) => Exit, // exit game + (Key { code: Spacebar, .. }, _, true) => { + // Do nothing, i.e. wait for the monster to come to you + TookTurn + } + (Key { code: Up, .. }, _, true) => { player_move_or_attack(0, -1, game, objects); TookTurn @@ -408,6 +426,29 @@ fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) -> P DidntTakeTurn } + (Key { code: Text, .. }, "c", true) => { + // Show player stats + let player = &objects[PLAYER]; + let level = player.level; + let level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR; + if let Some(fighter) = player.fighter.as_ref() { + let msg = format!( + "Character information + +Level: {} +Experience: {} +Experience to level up: {} + +Maximum HP: {}, +Attack: {}, +Defense: {}", + level, fighter.xp, level_up_xp, fighter.max_hp, fighter.power, fighter.defense + ); + msgbox(&msg, CHARACTER_SCREEN_WIDTH, &mut tcod.root); + } + DidntTakeTurn + } + _ => DidntTakeTurn, } } @@ -506,6 +547,7 @@ fn place_objects(room: Rect, map: &Map, objects: &mut Vec) { hp: 10, defense: 0, power: 3, + xp: 35, on_death: DeathCallback::Monster, }); orc.ai = Some(Ai::Basic); @@ -518,6 +560,7 @@ fn place_objects(room: Rect, map: &Map, objects: &mut Vec) { hp: 16, defense: 1, power: 4, + xp: 100, on_death: DeathCallback::Monster, }); troll.ai = Some(Ai::Basic); @@ -709,9 +752,14 @@ fn player_death(player: &mut Object, game: &mut Game) { 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); + game.messages.add( + format!( + "{} is dead! You gain {} experience points.", + monster.name, + monster.fighter.unwrap().xp + ), + ORANGE, + ); monster.char = '%'; monster.color = DARK_RED; monster.blocks = false; @@ -936,7 +984,9 @@ fn cast_lightning( ), LIGHT_BLUE, ); - objects[monster_id].take_damage(LIGHTNING_DAMAGE, game); + if let Some(xp) = objects[monster_id].take_damage(LIGHTNING_DAMAGE, game) { + objects[PLAYER].fighter.as_mut().unwrap().xp += xp; + } UseResult::UsedUp } else { // No enemy found within maximum range @@ -1007,7 +1057,8 @@ fn cast_fireball( ORANGE, ); - for obj in objects { + let mut xp_to_gain = 0; + for (id, obj) in objects.iter_mut().enumerate() { if obj.distance(x, y) <= FIREBALL_RADIUS as f32 && obj.fighter.is_some() { game.messages.add( format!( @@ -1016,9 +1067,15 @@ fn cast_fireball( ), ORANGE, ); - obj.take_damage(FIREBALL_DAMAGE, game); + if let Some(xp) = obj.take_damage(FIREBALL_DAMAGE, game) { + if id != PLAYER { + // Don't reward the player for burning themselves! + xp_to_gain += xp; + } + } } } + objects[PLAYER].fighter.as_mut().unwrap().xp += xp_to_gain; UseResult::UsedUp } @@ -1262,6 +1319,7 @@ fn new_game(tcod: &mut Tcod) -> (Game, Vec) { hp: 30, defense: 2, power: 5, + xp: 0, on_death: DeathCallback::Player, }); @@ -1308,6 +1366,9 @@ fn play_game(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) { tcod.root.flush(); + // Level up if needed + level_up(tcod, game, objects); + // Handle keys and exit game if needed previous_player_position = objects[PLAYER].pos(); let player_action = handle_keys(tcod, game, objects); @@ -1424,6 +1485,56 @@ fn next_level(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) { initialise_fov(tcod, &game.map); } +fn level_up(tcod: &mut Tcod, game: &mut Game, objects: &mut [Object]) { + let player = &mut objects[PLAYER]; + let level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR; + // Check if the player's experience is enough to level up + if player.fighter.as_ref().map_or(0, |f| f.xp) >= level_up_xp { + // It is, level up! + player.level += 1; + game.messages.add( + format!( + "Your battle skills grow stronger! You reached level {}!", + player.level + ), + YELLOW, + ); + + // Increase player's stats + let fighter = player.fighter.as_mut().unwrap(); + let mut choice = None; + while choice.is_none() { + // Keep asking until a choice is made + choice = menu( + "Level up! Choose a stat to raise:\n", + &[ + format!("Constitution (+ 20 HP, from {}", fighter.max_hp), + format!("Strength (+1 attack, from {}", fighter.power), + format!("Agility (+1 defense, from {}", fighter.defense), + ], + LEVEL_SCREEN_WIDTH, + &mut tcod.root, + ); + } + fighter.xp -= level_up_xp; + match choice.unwrap() { + 0 => { + fighter.max_hp += 20; + fighter.hp += 20; + } + + 1 => { + fighter.power += 1; + } + + 2 => { + fighter.defense += 1; + } + _ => unreachable!(), + } + } +} + fn main() { tcod::system::set_fps(FPS);