From 40f9d117f36ca8d1f105d8ca69411e83d08040be Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 21 Aug 2023 00:16:30 +0100 Subject: [PATCH] initial: character creation --- raws/items.json | 27 +++++ raws/spawn_tables.json | 8 ++ src/effects/hunger.rs | 9 +- src/gamesystem.rs | 37 ++++++ src/gui/mod.rs | 23 +++- src/gui/race_selection.rs | 229 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 27 ++++- src/spawner.rs | 72 ++---------- 8 files changed, 359 insertions(+), 73 deletions(-) create mode 100644 src/gui/race_selection.rs diff --git a/raws/items.json b/raws/items.json index 29c1d9a..f4d9860 100644 --- a/raws/items.json +++ b/raws/items.json @@ -137,6 +137,33 @@ "flags": ["EQUIP_MELEE", "STRENGTH"], "effects": { "base_damage": "1d6" } }, + { + "id": "equip_pitchfork", + "name": { "name": "pitchfork", "plural": "pitchforks" }, + "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "weight": 2, + "value": 5, + "flags": ["EQUIP_MELEE", "FINESSE"], + "effects": { "base_damage": "1d6" } + }, + { + "id": "equip_sickle", + "name": { "name": "sickle", "plural": "sickles" }, + "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "weight": 2, + "value": 5, + "flags": ["EQUIP_MELEE", "FINESSE"], + "effects": { "base_damage": "1d6" } + }, + { + "id": "equip_handaxe", + "name": { "name": "handaxe", "plural": "handaxes" }, + "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "weight": 2, + "value": 5, + "flags": ["EQUIP_MELEE", "FINESSE"], + "effects": { "base_damage": "1d6" } + }, { "id": "equip_longsword", "name": { "name": "longsword", "plural": "longswords" }, diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index a9e6f59..776b8e1 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -25,6 +25,14 @@ { "id": "equip_feet_iron", "weight": 2, "difficulty": 4} ] }, + { + "id": "villager_equipment", + "table": [ + { "id": "equip_pitchfork", "weight": 1, "difficulty": 1}, + { "id": "equip_sickle", "weight": 1, "difficulty": 1}, + { "id": "equip_handaxe", "weight": 1, "difficulty": 1} + ] + }, { "id": "potions", "table": [ diff --git a/src/effects/hunger.rs b/src/effects/hunger.rs index d46075c..0a32b6e 100644 --- a/src/effects/hunger.rs +++ b/src/effects/hunger.rs @@ -2,19 +2,16 @@ use super::{ triggers::{BLESSED, UNCURSED}, EffectSpawner, EffectType, }; -use crate::{HungerClock, HungerState}; +use crate::HungerClock; use specs::prelude::*; -const SATIATED_DURATION: i32 = 200; - pub fn restore_food(ecs: &mut World, effect: &EffectSpawner, target: Entity) { let buc = if let EffectType::RestoreNutrition { buc } = effect.effect_type { buc } else { UNCURSED }; if let Some(hc) = ecs.write_storage::().get_mut(target) { if buc == BLESSED || buc == UNCURSED { - hc.state = HungerState::Satiated; - hc.duration = SATIATED_DURATION; + hc.duration += 400; } else { - hc.duration = 0; + hc.duration += 200; } } } diff --git a/src/gamesystem.rs b/src/gamesystem.rs index 95ba43b..5e7c7c0 100644 --- a/src/gamesystem.rs +++ b/src/gamesystem.rs @@ -1,4 +1,6 @@ use super::{Skill, Skills}; +use crate::gui::Classes; +use rltk::prelude::*; /// Returns the attribute bonus for a given attribute score, where every 2 points above /// or below 10 is an additional +1 or -1. @@ -71,3 +73,38 @@ pub fn roll_4d6(rng: &mut rltk::RandomNumberGenerator) -> i32 { return roll; } + +/// Handles stat distribution for a player character. +pub fn get_attribute_rolls(rng: &mut RandomNumberGenerator, class: Classes) -> (i32, i32, i32, i32, i32, i32) { + let (mut str, mut dex, mut con, mut int, mut wis, mut cha) = match class { + Classes::Fighter => (10, 10, 10, 10, 10, 10), + Classes::Wizard => (10, 10, 10, 10, 10, 10), + Classes::Villager => (10, 10, 10, 10, 10, 10), + }; + let remaining_points = 75 - (str + dex + con + int + wis + cha); + let improve_chance: [i32; 6] = match class { + Classes::Fighter => [30, 20, 30, 6, 7, 7], + Classes::Wizard => [10, 20, 20, 30, 10, 10], + Classes::Villager => [15, 15, 40, 10, 10, 10], + }; + let improve_table = crate::random_table::RandomTable::new() + .add("Strength", improve_chance[0]) + .add("Dexterity", improve_chance[1]) + .add("Constitution", improve_chance[2]) + .add("Intelligence", improve_chance[3]) + .add("Wisdom", improve_chance[4]) + .add("Charisma", improve_chance[5]); + for _i in 0..remaining_points { + let roll = improve_table.roll(rng); + match roll.as_str() { + "Strength" => str += 1, + "Dexterity" => dex += 1, + "Constitution" => con += 1, + "Intelligence" => int += 1, + "Wisdom" => wis += 1, + "Charisma" => cha += 1, + _ => {} + } + } + return (str, dex, con, int, wis, cha); +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 9c7ce7b..feffeb5 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,17 +1,34 @@ use super::{ ai::CARRY_CAPACITY_PER_STRENGTH, camera, gamelog, gamesystem, hunger_system::get_hunger_colour, - hunger_system::get_hunger_state, rex_assets::RexAssets, ArmourClassBonus, Attributes, Burden, Charges, Equipped, - Hidden, HungerClock, HungerState, InBackpack, MagicItem, MagicItemClass, Map, MasterDungeonMap, Name, - ObfuscatedName, Player, Point, Pools, Position, Prop, Renderable, RunState, Skill, Skills, State, Viewshed, + rex_assets::RexAssets, ArmourClassBonus, Attributes, Burden, Charges, Equipped, Hidden, HungerClock, HungerState, + InBackpack, MagicItem, MagicItemClass, Map, MasterDungeonMap, Name, ObfuscatedName, Player, Point, Pools, Position, + Prop, Renderable, RunState, Skill, Skills, State, Viewshed, }; use rltk::prelude::*; use specs::prelude::*; use std::collections::BTreeMap; mod cheat_menu; mod letter_to_option; +mod race_selection; +pub use race_selection::*; mod tooltip; pub use cheat_menu::*; +/// Gives a popup box with a message and a title, and waits for a keypress. +#[allow(unused)] +pub fn yes_no(ctx: &mut Rltk, question: String) -> Option { + ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), question); + ctx.print_color_centered(17, RGB::named(rltk::CYAN), RGB::named(rltk::BLACK), "(y)es or (n)o"); + match ctx.key { + None => None, + Some(key) => match key { + VirtualKeyCode::Y => Some(true), + VirtualKeyCode::N => Some(false), + _ => None, + }, + } +} + pub fn draw_lerping_bar( ctx: &mut Rltk, sx: i32, diff --git a/src/gui/race_selection.rs b/src/gui/race_selection.rs new file mode 100644 index 0000000..321ca8e --- /dev/null +++ b/src/gui/race_selection.rs @@ -0,0 +1,229 @@ +use super::{gamesystem::attr_bonus, gamesystem::get_attribute_rolls, Attributes, Pools, Renderable, RunState, State}; +use crate::{ai::NORMAL_SPEED, raws, Attribute, Energy, Pool, Skill, Skills, Telepath}; +use rltk::prelude::*; +use specs::prelude::*; +use std::collections::HashMap; + +#[derive(PartialEq, Copy, Clone)] +pub enum Races { + NULL, + Human, + Dwarf, + Elf, +} + +#[derive(PartialEq, Copy, Clone)] +pub enum Classes { + Fighter, + Wizard, + Villager, +} + +#[derive(PartialEq, Copy, Clone)] +pub enum CharCreateResult { + NoSelection { race: Races, class: Classes }, + Selected { race: Races, class: Classes }, +} + +/// Handles the player character creation screen. +pub fn character_creation(gs: &mut State, ctx: &mut Rltk) -> CharCreateResult { + let runstate = gs.ecs.fetch::(); + + let mut x = 2; + let mut y = 11; + let column_width = 20; + + ctx.print_color(x, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Who are you? [Aa-Zz]"); + y += 2; + + if let RunState::CharacterCreation { race, class } = *runstate { + let selected_fg = RGB::named(GREEN); + let unselected_fg = RGB::named(WHITE); + let mut fg; + let bg = RGB::named(BLACK); + + // Races + if race == Races::Human { + fg = selected_fg; + } else { + fg = unselected_fg; + } + ctx.print_color(x, y, fg, bg, "h. Human"); + if race == Races::Elf { + fg = selected_fg; + } else { + fg = unselected_fg; + } + ctx.print_color(x, y + 1, fg, bg, "e. Elf"); + if race == Races::Dwarf { + fg = selected_fg; + } else { + fg = unselected_fg; + } + ctx.print_color(x, y + 2, fg, bg, "d. Dwarf"); + // Classes + x += column_width; + if class == Classes::Fighter { + fg = selected_fg; + } else { + fg = unselected_fg; + } + ctx.print_color(x, y, fg, bg, "f. Fighter"); + if class == Classes::Wizard { + fg = selected_fg; + } else { + fg = unselected_fg; + } + ctx.print_color(x, y + 1, fg, bg, "w. Wizard"); + if class == Classes::Villager { + fg = selected_fg; + } else { + fg = unselected_fg; + } + ctx.print_color(x, y + 2, fg, bg, "v. Villager"); + + match ctx.key { + None => return CharCreateResult::NoSelection { race, class }, + Some(key) => match key { + VirtualKeyCode::Escape => return CharCreateResult::Selected { race: Races::NULL, class }, + VirtualKeyCode::Return => return CharCreateResult::Selected { race, class }, + VirtualKeyCode::H => return CharCreateResult::NoSelection { race: Races::Human, class }, + VirtualKeyCode::E => return CharCreateResult::NoSelection { race: Races::Elf, class }, + VirtualKeyCode::D => return CharCreateResult::NoSelection { race: Races::Dwarf, class }, + VirtualKeyCode::F => return CharCreateResult::NoSelection { race, class: Classes::Fighter }, + VirtualKeyCode::W => return CharCreateResult::NoSelection { race, class: Classes::Wizard }, + VirtualKeyCode::V => return CharCreateResult::NoSelection { race, class: Classes::Villager }, + _ => return CharCreateResult::NoSelection { race, class }, + }, + } + } + return CharCreateResult::NoSelection { race: Races::Human, class: Classes::Fighter }; +} + +/// Handles player race setup. +pub fn setup_player_race(ecs: &mut World, race: Races) { + let player = ecs.fetch::(); + let mut renderables = ecs.write_storage::(); + // SKILLS + let mut skills = ecs.write_storage::(); + let player_skills = if let Some(skills) = skills.get_mut(*player) { + skills + } else { + skills.insert(*player, Skills { skills: HashMap::new() }).expect("Unable to insert skills component"); + skills.get_mut(*player).unwrap() + }; + match race { + Races::Human => {} + Races::Dwarf => { + renderables + .insert( + *player, + Renderable { + glyph: rltk::to_cp437('h'), + fg: RGB::named(rltk::RED), + bg: RGB::named(rltk::BLACK), + render_order: 0, + }, + ) + .expect("Unable to insert renderable component"); + *player_skills.skills.entry(Skill::Defence).or_insert(0) += 1; + } + Races::Elf => { + renderables + .insert( + *player, + Renderable { + glyph: rltk::to_cp437('@'), + fg: RGB::named(rltk::GREEN), + bg: RGB::named(rltk::BLACK), + render_order: 0, + }, + ) + .expect("Unable to insert renderable component"); + let mut telepaths = ecs.write_storage::(); + telepaths + .insert(*player, Telepath { telepath_tiles: Vec::new(), range: 6, dirty: true }) + .expect("Unable to insert telepath component"); + let mut speeds = ecs.write_storage::(); + speeds + .insert(*player, Energy { current: 0, speed: NORMAL_SPEED + 1 }) + .expect("Unable to insert energy component"); + } + _ => {} + } +} + +/// Handles player class setup +pub fn setup_player_class(ecs: &mut World, class: Classes) { + let player = *ecs.fetch::(); + // ATTRIBUTES + { + let mut rng = ecs.write_resource::(); + let mut attributes = ecs.write_storage::(); + + let (str, dex, con, int, wis, cha) = get_attribute_rolls(&mut rng, class); + attributes + .insert( + player, + Attributes { + strength: Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) }, + dexterity: Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) }, + constitution: Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) }, + intelligence: Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) }, + wisdom: Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) }, + charisma: Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) }, + }, + ) + .expect("Unable to insert attributes component"); + + let mut pools = ecs.write_storage::(); + pools + .insert( + player, + Pools { + hit_points: Pool { current: 10 + attr_bonus(con), max: 10 + attr_bonus(con) }, + mana: Pool { current: 2 + attr_bonus(int), max: 2 + attr_bonus(int) }, + xp: 0, + level: 1, + bac: 10, + weight: 0.0, + god: false, + }, + ) + .expect("Unable to insert pools component"); + } + + // TODO: use seeded RNG here + let mut rng = RandomNumberGenerator::new(); + let starts_with = get_starting_inventory(class, &mut rng); + for item in starts_with.0.iter() { + raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, item, raws::SpawnType::Equipped { by: player }, 0); + } + for item in starts_with.1.iter() { + raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, item, raws::SpawnType::Carried { by: player }, 0); + } +} + +fn get_starting_inventory(class: Classes, rng: &mut RandomNumberGenerator) -> (Vec, Vec) { + let mut equipped: Vec = Vec::new(); + let mut carried: Vec = Vec::new(); + match class { + Classes::Fighter => { + equipped = vec![ + "equip_shortsword".to_string(), + "equip_body_ringmail".to_string(), + "equip_mediumshield".to_string(), + ]; + carried = vec!["food_rations".to_string()]; + } + Classes::Wizard => { + equipped = vec!["equip_dagger".to_string(), "equip_back_protection".to_string()]; + } + Classes::Villager => { + let rolled_weapon = raws::table_by_name(&raws::RAWS.lock().unwrap(), "villager_equipment", 1).roll(rng); + equipped.push(rolled_weapon); + carried = vec!["food_rations".to_string(), "food_apple".to_string(), "food_apple".to_string()]; + } + } + return (equipped, carried); +} diff --git a/src/main.rs b/src/main.rs index 2458337..bf3113e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,7 +41,7 @@ extern crate lazy_static; //Consts pub const SHOW_MAPGEN: bool = false; pub const LOG_SPAWNING: bool = true; -pub const LOG_TICKS: bool = false; +pub const LOG_TICKS: bool = true; #[derive(PartialEq, Copy, Clone)] pub enum RunState { @@ -55,6 +55,7 @@ pub enum RunState { ShowTargeting { range: i32, item: Entity, aoe: i32 }, ActionWithDirection { function: fn(i: i32, j: i32, ecs: &mut World) -> RunState }, MainMenu { menu_selection: gui::MainMenuSelection }, + CharacterCreation { race: gui::Races, class: gui::Classes }, SaveGame, GameOver, NextLevel, @@ -209,6 +210,7 @@ impl GameState for State { match new_runstate { RunState::MainMenu { .. } => {} + RunState::CharacterCreation { .. } => {} _ => { // Draw map and ui camera::render_camera(&self.ecs, ctx); @@ -380,7 +382,10 @@ impl GameState for State { new_runstate = RunState::MainMenu { menu_selection: selected } } gui::MainMenuResult::Selected { selected } => match selected { - gui::MainMenuSelection::NewGame => new_runstate = RunState::PreRun, + gui::MainMenuSelection::NewGame => { + new_runstate = + RunState::CharacterCreation { race: gui::Races::Human, class: gui::Classes::Fighter } + } gui::MainMenuSelection::LoadGame => { saveload_system::load_game(&mut self.ecs); new_runstate = RunState::AwaitingInput; @@ -392,6 +397,23 @@ impl GameState for State { }, } } + RunState::CharacterCreation { .. } => { + let result = gui::character_creation(self, ctx); + match result { + gui::CharCreateResult::NoSelection { race, class } => { + new_runstate = RunState::CharacterCreation { race, class } + } + gui::CharCreateResult::Selected { race, class } => { + if race == gui::Races::NULL { + new_runstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame }; + } else { + gui::setup_player_race(&mut self.ecs, race); + gui::setup_player_class(&mut self.ecs, class); + new_runstate = RunState::PreRun; + } + } + } + } RunState::SaveGame => { saveload_system::save_game(&mut self.ecs); new_runstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::LoadGame }; @@ -596,6 +618,7 @@ fn main() -> rltk::BError { gs.ecs.insert(map::MasterDungeonMap::new()); // Master map list gs.ecs.insert(Map::new(1, 64, 64, 0, "New Map")); // Map gs.ecs.insert(Point::new(0, 0)); // Player pos + gs.ecs.insert(gui::Races::Dwarf); // Race let player_entity = spawner::player(&mut gs.ecs, 0, 0); gs.ecs.insert(player_entity); // Player entity gs.ecs.insert(RunState::MapGeneration {}); // RunState diff --git a/src/spawner.rs b/src/spawner.rs index fcbd662..04f9f84 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,7 +1,6 @@ use super::{ - ai::NORMAL_SPEED, gamesystem, gamesystem::attr_bonus, random_table::RandomTable, raws, Attribute, Attributes, - Clock, Energy, EquipmentChanged, Faction, HungerClock, HungerState, Map, Name, Player, Pool, Pools, Position, Rect, - Renderable, SerializeMe, Skill, Skills, TileType, Viewshed, + ai::NORMAL_SPEED, random_table::RandomTable, raws, Clock, Energy, EquipmentChanged, Faction, HungerClock, + HungerState, Map, Name, Player, Position, Rect, Renderable, SerializeMe, Skill, Skills, TileType, Viewshed, }; use rltk::{RandomNumberGenerator, RGB}; use specs::prelude::*; @@ -15,15 +14,6 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { skills.skills.insert(Skill::Defence, 0); skills.skills.insert(Skill::Magic, 0); - let mut rng = ecs.write_resource::(); - let str = gamesystem::roll_4d6(&mut rng); - let dex = gamesystem::roll_4d6(&mut rng); - let con = gamesystem::roll_4d6(&mut rng); - let int = gamesystem::roll_4d6(&mut rng); - let wis = gamesystem::roll_4d6(&mut rng); - let cha = gamesystem::roll_4d6(&mut rng); - std::mem::drop(rng); - // We only create the player once, so create the Clock here for counting turns too. ecs.create_entity().with(Clock {}).with(Energy { current: 0, speed: NORMAL_SPEED }).build(); let player = ecs @@ -40,13 +30,13 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { .with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true }) .with(Name { name: "you".to_string(), plural: "you".to_string() }) .with(HungerClock { state: HungerState::Satiated, duration: 1200 }) - .with(Attributes { - strength: Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) }, - dexterity: Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) }, - constitution: Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) }, - intelligence: Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) }, - wisdom: Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) }, - charisma: Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) }, + /*.with(Attributes { + strength: Attribute { base: 10, modifiers: 0, bonus: 0 }, + dexterity: Attribute { base: 10, modifiers: 0, bonus: 0 }, + constitution: Attribute { base: 10, modifiers: 0, bonus: 0 }, + intelligence: Attribute { base: 10, modifiers: 0, bonus: 0 }, + wisdom: Attribute { base: 10, modifiers: 0, bonus: 0 }, + charisma: Attribute { base: 10, modifiers: 0, bonus: 0 }, }) .with(Pools { hit_points: Pool { current: 10 + attr_bonus(con), max: 10 + attr_bonus(con) }, @@ -56,20 +46,13 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { bac: 10, weight: 0.0, god: false, - }) + })*/ .with(EquipmentChanged {}) .with(skills) .with(Energy { current: 0, speed: NORMAL_SPEED }) .marked::>() .build(); - raws::spawn_named_entity( - &raws::RAWS.lock().unwrap(), - ecs, - "equip_dagger", - raws::SpawnType::Equipped { by: player }, - 0, - ); raws::spawn_named_entity( &raws::RAWS.lock().unwrap(), ecs, @@ -77,41 +60,6 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { raws::SpawnType::Carried { by: player }, 0, ); - raws::spawn_named_entity( - &raws::RAWS.lock().unwrap(), - ecs, - "food_apple", - raws::SpawnType::Carried { by: player }, - 0, - ); - raws::spawn_named_entity( - &raws::RAWS.lock().unwrap(), - ecs, - "food_apple", - raws::SpawnType::Carried { by: player }, - 0, - ); - raws::spawn_named_entity( - &raws::RAWS.lock().unwrap(), - ecs, - "scroll_mass_health", - raws::SpawnType::Carried { by: player }, - 0, - ); - raws::spawn_named_entity( - &raws::RAWS.lock().unwrap(), - ecs, - "wand_fireball", - raws::SpawnType::Carried { by: player }, - 0, - ); - raws::spawn_named_entity( - &raws::RAWS.lock().unwrap(), - ecs, - "wand_fireball", - raws::SpawnType::Carried { by: player }, - 0, - ); return player; }