diff --git a/src/components.rs b/src/components.rs index 1625fbe..076cbca 100644 --- a/src/components.rs +++ b/src/components.rs @@ -348,6 +348,11 @@ pub struct Chasing { pub target: Entity, } +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Vendor { + pub categories: Vec, +} + // Serialization helper code. We need to implement ConvertSaveLoad for each type that contains an // Entity. diff --git a/src/gui.rs b/src/gui.rs index b7b6a3a..0a6bc31 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -4,12 +4,12 @@ use ::rltk::{Point, Rltk, VirtualKeyCode, RGB}; use ::specs::prelude::*; use crate::components::{ - Attribute, Attributes, Consumable, HungerClock, HungerState, InBackpack, Name, Pools, Position, - Viewshed, + Attribute, Attributes, Consumable, HungerClock, HungerState, InBackpack, Item, Name, Pools, + Position, Vendor, Viewshed, }; use crate::game_log::GameLog; use crate::rex_assets::RexAssets; -use crate::{camera, Equipped, Hidden, Map, RunState, State}; +use crate::{camera, Equipped, Hidden, Map, RunState, State, VendorMode}; pub fn draw_hollow_box( console: &mut Rltk, @@ -987,3 +987,211 @@ pub fn show_cheat_mode(_gs: &mut State, ctx: &mut Rltk) -> CheatMenuResult { }, } } + +#[derive(PartialEq, Copy, Clone)] +pub enum VendorResult { + NoResponse, + Cancel, + Sell, + BuyMode, + SellMode, + Buy, +} + +fn vendor_sell_menu( + gs: &mut State, + ctx: &mut Rltk, + _vendor: Entity, + _mode: VendorMode, +) -> (VendorResult, Option, Option, Option) { + let player_entity = gs.ecs.fetch::(); + let names = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + let items = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + + let inventory = (&backpack, &names) + .join() + .filter(|item| item.0.owner == *player_entity); + let count = inventory.count(); + + let mut y = (25 - (count / 2)) as i32; + ctx.draw_box( + 15, + y - 2, + 51, + (count + 3) as i32, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + ); + ctx.print_color( + 18, + y - 2, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + "Sell Which Item? (space to switch to buy mode)", + ); + ctx.print_color( + 18, + y + count as i32 + 1, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + "ESCAPE to cancel", + ); + + let mut equippable: Vec = Vec::new(); + for (j, (entity, _pack, name, item)) in (&entities, &backpack, &names, &items) + .join() + .filter(|item| item.1.owner == *player_entity) + .enumerate() + { + ctx.set( + 17, + y, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + rltk::to_cp437('('), + ); + ctx.set( + 18, + y, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + 97 + j as rltk::FontCharType, + ); + ctx.set( + 19, + y, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + rltk::to_cp437(')'), + ); + + ctx.print(21, y, &name.name.to_string()); + ctx.print(50, y, &format!("{:.1} gp", item.base_value * 0.8)); + equippable.push(entity); + y += 1; + } + + match ctx.key { + None => (VendorResult::NoResponse, None, None, None), + Some(key) => match key { + VirtualKeyCode::Space => (VendorResult::BuyMode, None, None, None), + VirtualKeyCode::Escape => (VendorResult::Cancel, None, None, None), + _ => { + let selection = rltk::letter_to_option(key); + if selection > -1 && selection < count as i32 { + return ( + VendorResult::Sell, + Some(equippable[selection as usize]), + None, + None, + ); + } + (VendorResult::NoResponse, None, None, None) + } + }, + } +} + +fn vendor_buy_menu( + gs: &mut State, + ctx: &mut Rltk, + vendor: Entity, + _mode: VendorMode, +) -> (VendorResult, Option, Option, Option) { + use crate::raws::*; + + let vendors = gs.ecs.read_storage::(); + + let inventory = get_vendor_items( + &vendors.get(vendor).unwrap().categories, + &RAWS.lock().unwrap(), + ); + let count = inventory.len(); + + let mut y = (25 - (count / 2)) as i32; + ctx.draw_box( + 15, + y - 2, + 51, + (count + 3) as i32, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + ); + ctx.print_color( + 18, + y - 2, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + "Buy Which Item? (space to switch to sell mode)", + ); + ctx.print_color( + 18, + y + count as i32 + 1, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + "ESCAPE to cancel", + ); + + for (j, sale) in inventory.iter().enumerate() { + ctx.set( + 17, + y, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + rltk::to_cp437('('), + ); + ctx.set( + 18, + y, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + 97 + j as rltk::FontCharType, + ); + ctx.set( + 19, + y, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + rltk::to_cp437(')'), + ); + + ctx.print(21, y, &sale.0); + ctx.print(50, y, &format!("{:.1} gp", sale.1 * 1.2)); + y += 1; + } + + match ctx.key { + None => (VendorResult::NoResponse, None, None, None), + Some(key) => match key { + VirtualKeyCode::Space => (VendorResult::SellMode, None, None, None), + VirtualKeyCode::Escape => (VendorResult::Cancel, None, None, None), + _ => { + let selection = ::rltk::letter_to_option(key); + if selection > -1 && selection < count as i32 { + return ( + VendorResult::Buy, + None, + Some(inventory[selection as usize].0.clone()), + Some(inventory[selection as usize].1), + ); + } + + (VendorResult::NoResponse, None, None, None) + } + }, + } +} + +pub fn show_vendor_menu( + gs: &mut State, + ctx: &mut Rltk, + vendor: Entity, + mode: VendorMode, +) -> (VendorResult, Option, Option, Option) { + match mode { + VendorMode::Buy => vendor_buy_menu(gs, ctx, vendor, mode), + VendorMode::Sell => vendor_sell_menu(gs, ctx, vendor, mode), + } +} diff --git a/src/main.rs b/src/main.rs index 682a2ce..a71286c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,7 @@ use map_indexing_system::MapIndexingSystem; use melee_combat_system::MeleeCombatSystem; use particle_system::ParticleSpawnSystem; use player::*; +use raws::*; pub use rect::Rect; use trigger_system::TriggerSystem; use visibility_system::VisibilitySystem; @@ -59,6 +60,12 @@ macro_rules! register { const SHOW_MAPGEN_VISUALIZER: bool = false; +#[derive(PartialEq, Copy, Clone)] +pub enum VendorMode { + Buy, + Sell, +} + #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, @@ -83,6 +90,10 @@ pub enum RunState { }, MapGeneration, ShowCheatMenu, + ShowVendor { + vendor: Entity, + mode: VendorMode, + }, } pub struct State { @@ -392,6 +403,60 @@ impl GameState for State { newrunstate = RunState::MapGeneration } }, + RunState::ShowVendor { vendor, mode } => { + let result = gui::show_vendor_menu(self, ctx, vendor, mode); + match result.0 { + gui::VendorResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::VendorResult::NoResponse => {} + gui::VendorResult::Sell => { + let price = self + .ecs + .read_storage::() + .get(result.1.unwrap()) + .unwrap() + .base_value + * 0.8; + self.ecs + .write_storage::() + .get_mut(*self.ecs.fetch::()) + .unwrap() + .gold += price; + self.ecs + .delete_entity(result.1.unwrap()) + .expect("Unable to delete sold item"); + } + gui::VendorResult::Buy => { + let tag = result.2.unwrap(); + let price = result.3.unwrap(); + let mut pools = self.ecs.write_storage::(); + let player_pools = pools.get_mut(*self.ecs.fetch::()).unwrap(); + if player_pools.gold >= price { + player_pools.gold -= price; + std::mem::drop(pools); + + let player_entity = *self.ecs.fetch::(); + spawn_named_item( + &RAWS.lock().unwrap(), + &mut self.ecs, + &tag, + SpawnType::Carried { by: player_entity }, + ); + } + } + gui::VendorResult::BuyMode => { + newrunstate = RunState::ShowVendor { + vendor, + mode: VendorMode::Buy, + } + } + gui::VendorResult::SellMode => { + newrunstate = RunState::ShowVendor { + vendor, + mode: VendorMode::Sell, + } + } + } + } } { @@ -509,6 +574,7 @@ fn main() -> ::rltk::BError { SingleActivation, Skills, SufferDamage, + Vendor, Viewshed, WantsToApproach, WantsToDropItem, diff --git a/src/player.rs b/src/player.rs index 24f4117..a159ade 100644 --- a/src/player.rs +++ b/src/player.rs @@ -5,11 +5,11 @@ use ::specs::prelude::*; use crate::components::{ Attributes, BlocksTile, BlocksVisibility, Door, EntityMoved, Faction, HungerClock, HungerState, - Item, Player, Pools, Position, Renderable, Viewshed, WantsToMelee, WantsToPickupItem, + Item, Player, Pools, Position, Renderable, Vendor, Viewshed, WantsToMelee, WantsToPickupItem, }; use crate::game_log::GameLog; use crate::raws::{self, Reaction, RAWS}; -use crate::{spatial, Map, RunState, State, TileType}; +use crate::{spatial, Map, RunState, State, TileType, VendorMode}; pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); @@ -25,6 +25,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState let mut blocks_movement = ecs.write_storage::(); let mut renderables = ecs.write_storage::(); let factions = ecs.read_storage::(); + let vendors = ecs.read_storage::(); let mut result = RunState::AwaitingInput; let mut swap_entities: Vec<(Entity, i32, i32)> = Vec::new(); @@ -43,6 +44,13 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState result = spatial::for_each_tile_content_with_gamemode(destination_idx, |potential_target| { + if vendors.get(potential_target).is_some() { + return Some(RunState::ShowVendor { + vendor: potential_target, + mode: VendorMode::Sell, + }); + } + let mut hostile = true; if combat_stats.get(potential_target).is_some() { if let Some(faction) = factions.get(potential_target) { diff --git a/src/raws/item_structs.rs b/src/raws/item_structs.rs index ef1d0b4..ad443d9 100644 --- a/src/raws/item_structs.rs +++ b/src/raws/item_structs.rs @@ -12,6 +12,7 @@ pub struct Item { pub initiative_penalty: Option, pub weight_lbs: Option, pub base_value: Option, + pub vendor_category: Option, } #[derive(Deserialize, Debug)] diff --git a/src/raws/mob_structs.rs b/src/raws/mob_structs.rs index 552f066..3d743f4 100644 --- a/src/raws/mob_structs.rs +++ b/src/raws/mob_structs.rs @@ -23,6 +23,7 @@ pub struct Mob { pub light: Option, pub faction: Option, pub gold: Option, + pub vendor: Option>, } #[derive(Deserialize, Debug)] diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 3347970..9cccc04 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -166,6 +166,20 @@ fn find_slot_for_equippable_item(tag: &str, raws: &RawMaster) -> EquipmentSlot { panic!("Trying to equip {}, but it has not slot tag", tag); } +pub fn get_vendor_items(categories: &[String], raws: &RawMaster) -> Vec<(String, f32)> { + let mut result = Vec::new(); + + for item in raws.raws.items.iter() { + if let Some(cat) = &item.vendor_category { + if categories.contains(cat) && item.base_value.is_some() { + result.push((item.name.clone(), item.base_value.unwrap())) + } + } + } + + result +} + fn spawn_position<'a>( pos: SpawnType, new_entity: EntityBuilder<'a>, @@ -406,7 +420,7 @@ pub fn spawn_named_mob( total_initiative_penalty: 0., gold: if let Some(gold) = &mob_template.gold { let mut rng = RandomNumberGenerator::new(); - let (n, d, b) = parse_dice_string(&gold); + let (n, d, b) = parse_dice_string(gold); (rng.roll_dice(n, d) + b) as f32 } else { 0.0 @@ -481,9 +495,15 @@ pub fn spawn_named_mob( eb = eb.with(Faction::from("Mindless")); } + if let Some(vendor) = &mob_template.vendor { + eb = eb.with(Vendor { + categories: vendor.clone(), + }) + } + let new_mob = eb.build(); - // Are they weilding anything? + // Are they wielding anything? if let Some(wielding) = &mob_template.equipped { for tag in wielding.iter() { spawn_named_entity(raws, ecs, tag, SpawnType::Equipped { by: new_mob }); diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 6254cb5..04ed736 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -104,6 +104,7 @@ pub fn save_game(ecs: &mut World) { SingleActivation, Skills, SufferDamage, + Vendor, Viewshed, WantsToApproach, WantsToDropItem, @@ -212,6 +213,7 @@ pub fn load_game(ecs: &mut World) { SingleActivation, Skills, SufferDamage, + Vendor, Viewshed, WantsToApproach, WantsToDropItem,