diff --git a/src/main.rs b/src/main.rs index b9bfc22..e534e0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ // #![allow(unused_variables)] // #![allow(unused_mut)] // #![allow(dead_code)] +use std::cmp; use rand::Rng; use serde::{Deserialize, Serialize}; -use std::cmp; use std::error::Error; use std::fs::File; use std::io::{Read, Write}; @@ -14,11 +14,48 @@ use tcod::console::*; use tcod::input::{self, Event, Key, Mouse}; use tcod::map::{FovAlgorithm, Map as FovMap}; +// Size of the window const SCREEN_WIDTH: i32 = 80; const SCREEN_HEIGHT: i32 = 50; -const FPS: i32 = 20; + +// Size of the map const MAP_WIDTH: i32 = 80; const MAP_HEIGHT: i32 = 43; + +// 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 INVENTORY_WIDTH: i32 = 50; +const CHARACTER_SCREEN_WIDTH: i32 = 30; +const LEVEL_SCREEN_WIDTH: i32 = 40; + +// Parameters for the dungeon generator +const ROOM_MAX_SIZE: i32 = 10; +const ROOM_MIN_SIZE: i32 = 6; +const MAX_ROOMS: i32 = 30; + +const HEAL_AMOUNT: i32 = 40; +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 = 25; + +// Experience and level-ups +const LEVEL_UP_BASE: i32 = 200; +const LEVEL_UP_FACTOR: i32 = 150; + +const FOV_ALGO: FovAlgorithm = FovAlgorithm::Basic; +const FOV_LIGHT_WALLS: bool = true; +const TORCH_RADIUS: i32 = 10; + +const FPS: i32 = 20; + const COLOR_DARK_WALL: Color = Color { r: 0, g: 0, b: 100 }; const COLOR_LIGHT_WALL: Color = Color { r: 130, @@ -35,35 +72,11 @@ const COLOR_LIGHT_GROUND: Color = Color { 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; + +// Player will always be the first object 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; -// 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; + +type Map = Vec>; struct Tcod { root: Root, @@ -216,8 +229,6 @@ impl Tile { } } -type Map = Vec>; - #[derive(Serialize, Deserialize)] struct Game { map: Map, @@ -288,6 +299,11 @@ impl Messages { } } +struct Transition { + level: u32, + value: u32, +} + #[derive(Clone, Copy, Debug, PartialEq)] enum PlayerAction { TookTurn, @@ -453,7 +469,7 @@ Defense: {}", } } -fn make_map(objects: &mut Vec) -> Map { +fn make_map(objects: &mut Vec, level: u32) -> Map { // Fill the map with unblocked tiles let mut map = vec![vec![Tile::wall(); MAP_HEIGHT as usize]; MAP_WIDTH as usize]; @@ -477,7 +493,7 @@ fn make_map(objects: &mut Vec) -> Map { if !failed { create_room(new_room, &mut map); - place_objects(new_room, &map, objects); + place_objects(new_room, &map, objects, level); let (new_x, new_y) = new_room.center(); if rooms.is_empty() { @@ -530,41 +546,139 @@ fn create_v_tunnel(y1: i32, y2: i32, x: i32, map: &mut Map) { } } -fn place_objects(room: Rect, map: &Map, objects: &mut Vec) { +fn place_objects(room: Rect, map: &Map, objects: &mut Vec, level: u32) { + use rand::distributions::{IndependentSample, Weighted, WeightedChoice}; + + // Maximum number of monsters per room + let max_monsters = from_dungeon_level( + &[ + Transition { level: 1, value: 2 }, + Transition { level: 4, value: 3 }, + Transition { level: 6, value: 5 }, + ], + level, + ); + // Generate some monsters - let num_monsters = rand::thread_rng().gen_range(0, MAX_ROOM_MONSTERS + 1); + let num_monsters = rand::thread_rng().gen_range(0, max_monsters + 1); + + // Monster random table + let troll_chance = from_dungeon_level( + &[ + Transition { + level: 3, + value: 15, + }, + Transition { + level: 5, + value: 30, + }, + Transition { + level: 7, + value: 60, + }, + ], + level, + ); + + let monster_chances = &mut [ + Weighted { + weight: 80, + item: "orc", + }, + Weighted { + weight: troll_chance, + item: "troll", + }, + ]; + let monster_choice = WeightedChoice::new(monster_chances); + + // Maximum number of items per room + let max_items = from_dungeon_level( + &[ + Transition { level: 1, value: 1 }, + Transition { level: 4, value: 2 }, + ], + level, + ); + + // Item random table + let item_chances = &mut [ + Weighted { + weight: 35, + item: Item::Heal, + }, + Weighted { + weight: from_dungeon_level( + &[Transition { + level: 4, + value: 25, + }], + level, + ), + item: Item::Lightning, + }, + Weighted { + weight: from_dungeon_level( + &[Transition { + level: 6, + value: 25, + }], + level, + ), + item: Item::Fireball, + }, + Weighted { + weight: from_dungeon_level( + &[Transition { + level: 2, + value: 10, + }], + level, + ), + item: Item::Confuse, + }, + ]; + + let item_choice = WeightedChoice::new(item_chances); 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::() < 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, - xp: 35, - 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, - xp: 100, - on_death: DeathCallback::Monster, - }); - troll.ai = Some(Ai::Basic); - troll + let mut monster = match monster_choice.ind_sample(&mut rand::thread_rng()) { + "orc" => { + // Create an orc + let mut orc = Object::new(x, y, 'o', "orc", DESATURATED_GREEN, true); + orc.fighter = Some(Fighter { + max_hp: 20, + hp: 20, + defense: 0, + power: 4, + xp: 35, + on_death: DeathCallback::Monster, + }); + orc.ai = Some(Ai::Basic); + orc + } + + "troll" => { + // Create a troll + let mut troll = Object::new(x, y, 'T', "troll", DARKER_GREEN, true); + troll.fighter = Some(Fighter { + max_hp: 30, + hp: 30, + defense: 2, + power: 8, + xp: 100, + on_death: DeathCallback::Monster, + }); + troll.ai = Some(Ai::Basic); + troll + } + + _ => unreachable!(), }; monster.alive = true; @@ -573,7 +687,7 @@ fn place_objects(room: Rect, map: &Map, objects: &mut Vec) { } // Generate some items - let num_items = rand::thread_rng().gen_range(0, MAX_ROOM_ITEMS + 1); + let num_items = rand::thread_rng().gen_range(0, max_items + 1); for _ in 0..num_items { // Choose random spot for this item @@ -582,27 +696,31 @@ fn place_objects(room: Rect, map: &Map, objects: &mut Vec) { // Only place it if the tile is note blocked if !is_blocked(x, y, map, objects) { - let dice = rand::random::(); - let mut 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 + let mut item = match item_choice.ind_sample(&mut rand::thread_rng()) { + Item::Heal => { + // Create a healing potion + let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false); + object.item = Some(Item::Heal); + object + } + Item::Lightning => { + // Create a lightning bolt scroll + let mut object = Object::new(x, y, '#', "scroll of lightning bolt", LIGHT_YELLOW, false); + object.item = Some(Item::Lightning); + object + } + Item::Fireball => { + // Create a fireball scroll + let mut object = Object::new(x, y, '#', "scroll of fireball", LIGHT_YELLOW, false); + object.item = Some(Item::Fireball); + object + } + Item::Confuse => { + // Create a confuse scroll + let mut object = Object::new(x, y, '#', "scroll of confusion", LIGHT_YELLOW, false); + object.item = Some(Item::Confuse); + object + } }; item.always_visible = true; @@ -1315,10 +1433,10 @@ fn new_game(tcod: &mut Tcod) -> (Game, Vec) { 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, + max_hp: 100, + hp: 100, + defense: 1, + power: 4, xp: 0, on_death: DeathCallback::Player, }); @@ -1328,7 +1446,7 @@ fn new_game(tcod: &mut Tcod) -> (Game, Vec) { let mut game = Game { // Generate map - map: make_map(&mut objects), + map: make_map(&mut objects, 1), messages: Messages::new(), inventory: vec![], dungeon_level: 1, @@ -1481,7 +1599,7 @@ fn next_level(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) { RED, ); game.dungeon_level += 1; - game.map = make_map(objects); + game.map = make_map(objects, game.dungeon_level); initialise_fov(tcod, &game.map); } @@ -1535,6 +1653,16 @@ fn level_up(tcod: &mut Tcod, game: &mut Game, objects: &mut [Object]) { } } +// Returns a value that depends on level. The table specifies what +// value occurs after each level, default is 0. +fn from_dungeon_level(table: &[Transition], level: u32) -> u32 { + table + .iter() + .rev() + .find(|transition| level >= transition.level) + .map_or(0, |transition| transition.value) +} + fn main() { tcod::system::set_fps(FPS);