diff --git a/raws/items.json b/raws/items.json index ffb6d8d..118c940 100644 --- a/raws/items.json +++ b/raws/items.json @@ -19,14 +19,23 @@ "effects": { "healing": "2d4+2" }, "magic": { "class": "uncommon", "naming": "potion" } }, + { + "id": "scroll_identify", + "name": { "name": "scroll of identify", "plural": "scrolls of identify" }, + "renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 }, + "weight": 0.5, + "value": 100, + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "IDENTIFY"], + "magic": { "class": "uncommon", "naming": "scroll" } + }, { "id": "scroll_removecurse", "name": { "name": "scroll of remove curse", "plural": "scrolls of remove curse" }, "renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 }, "weight": 0.5, - "value": 50, + "value": 200, "flags": ["CONSUMABLE", "DESTRUCTIBLE", "REMOVE_CURSE"], - "magic": { "class": "uncommon", "naming": "scroll" } + "magic": { "class": "rare", "naming": "scroll" } }, { "id": "scroll_health", diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index 39fa53d..d2e6823 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -47,6 +47,7 @@ { "id": "scrolls", "table": [ + { "id": "scroll_identify", "weight": 3000, "difficulty": 1}, { "id": "scroll_removecurse", "weight": 2, "difficulty": 1}, { "id": "scroll_confusion", "weight": 2, "difficulty": 1}, { "id": "scroll_magicmap", "weight": 2, "difficulty": 1}, diff --git a/src/components.rs b/src/components.rs index 6149cf0..d608adb 100644 --- a/src/components.rs +++ b/src/components.rs @@ -451,7 +451,10 @@ pub struct SpawnParticleBurst { pub struct Destructible {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] -pub struct RemovesCurse {} +pub struct ProvidesRemoveCurse {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ProvidesIdentify {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Digger {} diff --git a/src/effects/messages.rs b/src/effects/messages.rs index 2116ad0..b910f87 100644 --- a/src/effects/messages.rs +++ b/src/effects/messages.rs @@ -1,6 +1,9 @@ pub const NOCHARGES_WREST: &str = "You wrest one last charge from the worn-out wand."; pub const NOCHARGES_DIDNOTHING: &str = "The wand does nothing."; +pub const IDENTIFY_ALL: &str = "You feel attuned to your belongings!"; +pub const IDENTIFY_ALL_BLESSED: &str = "Divine favour reveals all"; + pub const REMOVECURSE: &str = "You feel a weight lifted!"; pub const REMOVECURSE_BLESSED: &str = "You feel righteous"; pub const REMOVECURSE_BLESSED_FAILED: &str = "You feel righteous! But nothing happened."; diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index 35ab17a..ec03ef7 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -1,9 +1,9 @@ use super::{add_effect, get_noncursed, messages::*, particles, spatial, EffectType, Entity, Targets, World}; use crate::{ gamelog, gui::item_colour_ecs, gui::obfuscate_name_ecs, gui::renderable_colour, Beatitude, Charges, Confusion, - Consumable, Destructible, Equipped, Hidden, InBackpack, InflictsDamage, Item, MagicMapper, Player, Prop, - ProvidesHealing, ProvidesNutrition, RandomNumberGenerator, RemovesCurse, Renderable, RunState, SingleActivation, - BUC, + Consumable, Destructible, Equipped, Hidden, InBackpack, InflictsDamage, Item, MagicMapper, MasterDungeonMap, Name, + ObfuscatedName, Player, Prop, ProvidesHealing, ProvidesIdentify, ProvidesNutrition, ProvidesRemoveCurse, + RandomNumberGenerator, Renderable, RunState, SingleActivation, BUC, }; use rltk::prelude::*; use specs::prelude::*; @@ -68,11 +68,12 @@ fn event_trigger(source: Option, entity: Entity, target: &Targets, ecs: let (logger, restored_nutrition) = handle_restore_nutrition(ecs, &mut event, logger); let (logger, magic_mapped) = handle_magic_mapper(ecs, &mut event, logger); let (logger, removed_curse) = handle_remove_curse(ecs, &mut event, logger); + let (logger, identified) = handle_identify(ecs, &mut event, logger); let (logger, healed) = handle_healing(ecs, &mut event, logger); let (logger, damaged) = handle_damage(ecs, &mut event, logger); let (logger, confused) = handle_confusion(ecs, &mut event, logger); //let (logger, dug) = handle_dig(ecs, &mut event, logger); -- NYI i.e. Wand of Digging - did_something |= restored_nutrition || magic_mapped || healed || damaged || confused || removed_curse; + did_something |= restored_nutrition || magic_mapped || healed || damaged || confused || removed_curse || identified; if event.log { logger.log(); @@ -208,18 +209,59 @@ fn handle_confusion(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog: return (logger, false); } -fn select_single_remove_curse(ecs: &World) { - let mut runstate = ecs.fetch_mut::(); - *runstate = RunState::ShowRemoveCurse; +fn select_single(ecs: &World, runstate: RunState) { + let mut new_runstate = ecs.fetch_mut::(); + *new_runstate = runstate; +} + +fn handle_identify(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { + if let Some(_i) = ecs.read_storage::().get(event.entity) { + let mut rng = ecs.write_resource::(); + let mut dm = ecs.fetch_mut::(); + let identify_all = match event.buc { + BUC::Blessed => rng.roll_dice(1, 5) == 1, + BUC::Uncursed => rng.roll_dice(1, 25) == 1, + _ => false, + }; + if !identify_all { + select_single(ecs, RunState::ShowIdentify); + return (logger, true); + } + let mut to_identify: Vec<(Entity, String)> = Vec::new(); + for (e, _i, _bp, _o, name) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .filter(|(_e, _i, bp, _o, name)| { + bp.owner == event.source.unwrap() && !dm.identified_items.contains(&name.name.clone()) + }) + { + to_identify.push((e, name.name.clone())); + } + for item in to_identify { + dm.identified_items.insert(item.1); + if let Some(beatitude) = ecs.write_storage::().get_mut(item.0) { + beatitude.known = true; + } + } + logger = logger.append(IDENTIFY_ALL).buc(event.buc.clone(), None, Some(IDENTIFY_ALL_BLESSED)); + event.log = true; + return (logger, true); + } + return (logger, false); } fn handle_remove_curse(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { - if let Some(_r) = ecs.read_storage::().get(event.entity) { + if let Some(_r) = ecs.read_storage::().get(event.entity) { let mut to_decurse: Vec = Vec::new(); match event.buc { // If cursed, show the prompt to select one item. BUC::Cursed => { - select_single_remove_curse(ecs); + select_single(ecs, RunState::ShowRemoveCurse); return (logger, true); } // If blessed, decurse everything in our backpack. @@ -252,7 +294,7 @@ fn handle_remove_curse(ecs: &mut World, event: &mut EventInfo, mut logger: gamel } if to_decurse.len() == 0 { match event.buc { - BUC::Uncursed => select_single_remove_curse(ecs), + BUC::Uncursed => select_single(ecs, RunState::ShowRemoveCurse), BUC::Blessed => logger = logger.append(REMOVECURSE_BLESSED_FAILED), _ => {} } diff --git a/src/gui/identify_menu.rs b/src/gui/identify_menu.rs new file mode 100644 index 0000000..5dddd7c --- /dev/null +++ b/src/gui/identify_menu.rs @@ -0,0 +1,122 @@ +use super::{ + get_max_inventory_width, item_colour_ecs, obfuscate_name_ecs, print_options, renderable_colour, ItemMenuResult, + UniqueInventoryItem, +}; +use crate::{ + gamelog, Beatitude, Entity, Equipped, InBackpack, Item, MasterDungeonMap, Name, ObfuscatedName, Renderable, State, + BUC, +}; +use rltk::prelude::*; +use specs::prelude::*; +use std::collections::BTreeMap; + +/// Handles the Identify menu. +pub fn identify(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option) { + let player_entity = gs.ecs.fetch::(); + let equipped = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + let items = gs.ecs.read_storage::(); + let obfuscated = gs.ecs.read_storage::(); + let dm = gs.ecs.fetch::(); + let names = gs.ecs.read_storage::(); + let renderables = gs.ecs.read_storage::(); + + let build_identify_iterator = || { + (&entities, &items, &renderables, &names).join().filter(|(item_entity, _i, _r, n)| { + // If not owned by the player, return false. + let mut keep = false; + if let Some(bp) = backpack.get(*item_entity) { + if bp.owner == *player_entity { + keep = true; + } + } + // If not equipped by the player, return false. + if let Some(equip) = equipped.get(*item_entity) { + if equip.owner == *player_entity { + keep = true; + } + } + if !keep { + return false; + } + // If not obfuscated, or already identified, return false. + if !obfuscated.get(*item_entity).is_some() || dm.identified_items.contains(&n.name) { + return false; + } + return true; + }) + }; + + // Build list of items to display + let count = build_identify_iterator().count(); + // If no items, return nothing, wasting the scroll. + if count == 0 { + gamelog::Logger::new().append("You've got nothing to identify! Know-it-all.").log(); + return (ItemMenuResult::Cancel, None); + } + // If only one item, return it. + if count == 1 { + let item = build_identify_iterator().nth(0).unwrap().0; + gamelog::Logger::new() + .append("You identify the") + .colour(item_colour_ecs(&gs.ecs, item)) + .append_n(obfuscate_name_ecs(&gs.ecs, item).0) + .colour(WHITE) + .append("!") + .log(); + return (ItemMenuResult::Selected, Some(build_identify_iterator().nth(0).unwrap().0)); + } + let mut player_inventory: BTreeMap = BTreeMap::new(); + let mut inventory_ids: BTreeMap = BTreeMap::new(); + for (entity, _i, renderable, name) in build_identify_iterator() { + let (singular, plural) = obfuscate_name_ecs(&gs.ecs, entity); + player_inventory + .entry(UniqueInventoryItem { + display_name: super::DisplayName { singular: singular.clone(), plural: plural.clone() }, + rgb: item_colour_ecs(&gs.ecs, entity), + renderables: renderable_colour(&renderables, entity), + glyph: renderable.glyph, + name: name.name.clone(), + }) + .and_modify(|count| *count += 1) + .or_insert(1); + inventory_ids.entry(singular).or_insert(entity); + } + // Get display args + let width = get_max_inventory_width(&player_inventory); + let (_, _, _, _, x_offset, y_offset) = crate::camera::get_screen_bounds(&gs.ecs, ctx); + let (x, y) = (x_offset + 1, y_offset + 3); + // Draw menu + ctx.print_color( + 1 + x_offset, + 1 + y_offset, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + "Identify which item? [aA-zZ][Esc.]", + ); + ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK)); + print_options(player_inventory, x + 1, y + 1, ctx); + // Input + match ctx.key { + None => (ItemMenuResult::NoResponse, None), + Some(key) => match key { + VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), + _ => { + let selection = rltk::letter_to_option(key); + if selection > -1 && selection < count as i32 { + let item = inventory_ids.iter().nth(selection as usize).unwrap().1; + gamelog::Logger::new() + .append("You identify the") + .colour(item_colour_ecs(&gs.ecs, *item)) + .append_n(obfuscate_name_ecs(&gs.ecs, *item).0) + .colour(WHITE) + .append("!") + .log(); + return (ItemMenuResult::Selected, Some(*item)); + } + (ItemMenuResult::NoResponse, None) + } + }, + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 134acb8..9d25c63 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -13,6 +13,8 @@ mod letter_to_option; pub use character_creation::*; mod remove_curse_menu; pub use remove_curse_menu::*; +mod identify_menu; +pub use identify_menu::*; mod tooltip; pub use cheat_menu::*; diff --git a/src/gui/remove_curse_menu.rs b/src/gui/remove_curse_menu.rs index 786d6c8..4e6ba34 100644 --- a/src/gui/remove_curse_menu.rs +++ b/src/gui/remove_curse_menu.rs @@ -52,11 +52,20 @@ pub fn remove_curse(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option = BTreeMap::new(); let mut inventory_ids: BTreeMap = BTreeMap::new(); diff --git a/src/main.rs b/src/main.rs index 346652a..cf3b7ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ pub enum RunState { ShowRemoveItem, ShowTargeting { range: i32, item: Entity, aoe: i32 }, ShowRemoveCurse, + ShowIdentify, ActionWithDirection { function: fn(i: i32, j: i32, ecs: &mut World) -> RunState }, MainMenu { menu_selection: gui::MainMenuSelection }, CharacterCreation { ancestry: gui::Ancestry, class: gui::Class }, @@ -263,6 +264,7 @@ impl GameState for State { new_runstate = RunState::MagicMapReveal { row: row, cursed: cursed } } RunState::ShowRemoveCurse => new_runstate = RunState::ShowRemoveCurse, + RunState::ShowIdentify => new_runstate = RunState::ShowIdentify, _ => new_runstate = RunState::Ticking, } } @@ -397,6 +399,24 @@ impl GameState for State { } } } + RunState::ShowIdentify => { + let result = gui::identify(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => new_runstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + if let Some(name) = self.ecs.read_storage::().get(item_entity) { + let mut dm = self.ecs.fetch_mut::(); + dm.identified_items.insert(name.name.clone()); + } + if let Some(beatitude) = self.ecs.write_storage::().get_mut(item_entity) { + beatitude.known = true; + } + new_runstate = RunState::Ticking; + } + } + } RunState::ActionWithDirection { function } => { new_runstate = gui::get_input_direction(&mut self.ecs, ctx, function); } @@ -637,7 +657,8 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); - gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 80262de..8e9a74f 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -60,7 +60,8 @@ macro_rules! apply_flags { "FOOD" => $eb = $eb.with(ProvidesNutrition {}), "CONSUMABLE" => $eb = $eb.with(Consumable {}), "CHARGES" => $eb = $eb.with(Charges { uses: 3, max_uses: 3 }), - "REMOVE_CURSE" => $eb = $eb.with(RemovesCurse {}), + "REMOVE_CURSE" => $eb = $eb.with(ProvidesRemoveCurse {}), + "IDENTIFY" => $eb = $eb.with(ProvidesIdentify {}), "DIGGER" => $eb = $eb.with(Digger {}), "MAGICMAP" => $eb = $eb.with(MagicMapper {}), // CAN BE DESTROYED BY DAMAGE diff --git a/src/saveload_system.rs b/src/saveload_system.rs index d191fa1..08aed29 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -99,10 +99,11 @@ pub fn save_game(ecs: &mut World) { Position, Prop, ProvidesHealing, + ProvidesIdentify, ProvidesNutrition, + ProvidesRemoveCurse, Quips, Ranged, - RemovesCurse, Renderable, SingleActivation, Skills, @@ -225,10 +226,11 @@ pub fn load_game(ecs: &mut World) { Position, Prop, ProvidesHealing, + ProvidesIdentify, ProvidesNutrition, + ProvidesRemoveCurse, Quips, Ranged, - RemovesCurse, Renderable, SingleActivation, Skills,