diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index 5292a22..39fa53d 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -33,8 +33,8 @@ "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": "equip_sickle", "weight": 1, "difficulty": 1}, + { "id": "equip_handaxe", "weight": 1, "difficulty": 1} ] }, { diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index 7889ec1..c134fb3 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -1,8 +1,9 @@ -use super::{add_effect, get_noncursed, particles, spatial, targeting, EffectType, Entity, Targets, World}; +use super::{add_effect, get_noncursed, 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, Hidden, InflictsDamage, Item, MagicMapper, Player, Prop, ProvidesHealing, - ProvidesNutrition, RandomNumberGenerator, Renderable, RunState, SingleActivation, BUC, + Consumable, Destructible, Equipped, Hidden, InBackpack, InflictsDamage, Item, MagicMapper, Player, Prop, + ProvidesHealing, ProvidesNutrition, RandomNumberGenerator, RemovesCurse, Renderable, RunState, SingleActivation, + BUC, }; use rltk::prelude::*; use specs::prelude::*; @@ -61,14 +62,17 @@ fn event_trigger(source: Option, entity: Entity, target: &Targets, ecs: let logger = gamelog::Logger::new(); let mut did_something = false; + particles::handle_simple_particles(ecs, entity, target); particles::handle_burst_particles(ecs, entity, &target); particles::handle_line_particles(ecs, entity, &target); 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, 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); - did_something |= restored_nutrition || magic_mapped || healed || damaged || confused; + //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; if event.log { logger.log(); @@ -208,6 +212,72 @@ 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 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) { + let mut to_decurse: Vec = Vec::new(); + let mut cursed_in_backpack = false; + let mut cursed_in_inventory = false; + match event.buc { + // If cursed, show the prompt to select one item. + BUC::Cursed => { + select_single_remove_curse(ecs); + return (logger, true); + } + // If blessed, decurse everything in our backpack. + BUC::Blessed => { + for (entity, _i, _bp, _b) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .filter(|(_e, _i, bp, b)| bp.owner == event.source.unwrap() && b.buc == BUC::Cursed) + { + cursed_in_backpack = true; + to_decurse.push(entity); + } + } + _ => {} + } + // If noncursed, decurse everything we have equipped. + for (entity, _i, _e, _b) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .filter(|(_e, _i, e, b)| e.owner == event.source.unwrap() && b.buc == BUC::Cursed) + { + cursed_in_inventory = true; + to_decurse.push(entity); + } + if to_decurse.len() == 0 { + match event.buc { + BUC::Uncursed => select_single_remove_curse(ecs), + BUC::Blessed => logger = logger.append("You feel righteous! ... but nothing happens."), + _ => {} + } + return (logger, true); + } + let mut beatitudes = ecs.write_storage::(); + for e in to_decurse { + beatitudes.insert(e, Beatitude { buc: BUC::Uncursed, known: true }).expect("Unable to insert beatitude"); + } + logger = + logger.append("You feel a reassuring presence.").buc(event.buc.clone(), None, Some("You feel righteous!")); + event.log = true; + return (logger, true); + } + return (logger, false); +} + fn get_entity_targets(target: &Targets) -> Vec { let mut entities: Vec = Vec::new(); match target { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 0c037a7..5166ed8 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -11,6 +11,8 @@ mod character_creation; mod cheat_menu; mod letter_to_option; pub use character_creation::*; +mod remove_curse_menu; +pub use remove_curse_menu::*; mod tooltip; pub use cheat_menu::*; @@ -391,15 +393,18 @@ pub fn get_max_inventory_width(inventory: &BTreeMap) - let mut width: i32 = 0; for (item, count) in inventory { let mut this_width = item.display_name.singular.len() as i32; - if count < &1 { - this_width += 6; - if item.display_name.singular.ends_with("s") { - this_width += 5; + // Clean this up. It should use consts. + this_width += 4; // The spaces before and after the character to select this item, etc. + if count <= &1 { + if item.display_name.singular == item.display_name.plural { + this_width += 4; // "some".len } else if ['a', 'e', 'i', 'o', 'u'].iter().any(|&v| item.display_name.singular.starts_with(v)) { - this_width += 3; + this_width += 2; // "an".len + } else { + this_width += 1; // "a".len } } else { - this_width += 6; + this_width += count.to_string().len() as i32; // i.e. "12".len } width = if width > this_width { width } else { this_width }; } diff --git a/src/gui/remove_curse_menu.rs b/src/gui/remove_curse_menu.rs new file mode 100644 index 0000000..786d6c8 --- /dev/null +++ b/src/gui/remove_curse_menu.rs @@ -0,0 +1,113 @@ +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, Name, Renderable, State, BUC}; +use rltk::prelude::*; +use specs::prelude::*; +use std::collections::BTreeMap; + +/// Handles the Remove Curse menu. +pub fn remove_curse(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 beatitudes = gs.ecs.read_storage::(); + let names = gs.ecs.read_storage::(); + let renderables = gs.ecs.read_storage::(); + + let build_cursed_iterator = || { + (&entities, &items, &beatitudes, &renderables, &names).join().filter(|(item_entity, _i, b, _r, _n)| { + // Set all items to FALSE initially. + let mut keep = false; + // If found in the player's backpack, set to TRUE + if let Some(bp) = backpack.get(*item_entity) { + if bp.owner == *player_entity { + keep = true; + } + } + // If found in the player's equipslot, set to TRUE + if let Some(equip) = equipped.get(*item_entity) { + if equip.owner == *player_entity { + keep = true; + } + } + // If it's not OUR item, RETURN FALSE. + if !keep { + return false; + } + // If it's identified as noncursed, RETURN FALSE. + if b.known && b.buc != BUC::Cursed { + return false; + } + // Otherwise, return: returns any items that are unidentified, + // or identified as being cursed. + return true; + }) + }; + + // Build list of items to display + let count = build_cursed_iterator().count(); + // If no items, return nothing, wasting the scroll. + if count == 0 { + return (ItemMenuResult::Cancel, None); + } + // If only one item, return it. + if count == 1 { + return (ItemMenuResult::Selected, Some(build_cursed_iterator().nth(0).unwrap().0)); + } + let mut player_inventory: BTreeMap = BTreeMap::new(); + let mut inventory_ids: BTreeMap = BTreeMap::new(); + for (entity, _i, _b, renderable, name) in build_cursed_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), + "Decurse 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 decurse 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/inventory/equip_system.rs b/src/inventory/equip_system.rs index c1105bb..758939b 100644 --- a/src/inventory/equip_system.rs +++ b/src/inventory/equip_system.rs @@ -89,11 +89,12 @@ impl<'a> System<'a> for ItemEquipSystem { if target == *player_entity { logger = logger .append("You remove your") + .colour(item_colour(*item, &beatitudes, &dm)) .append_n( obfuscate_name(*item, &names, &magic_items, &obfuscated_names, &beatitudes, &dm, None) .0, ) - .colour(item_colour(*item, &beatitudes, &dm)) + .colour(rltk::WHITE) .period(); } } diff --git a/src/main.rs b/src/main.rs index 6aa2e11..60e96c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,7 @@ pub enum RunState { ShowDropItem, ShowRemoveItem, ShowTargeting { range: i32, item: Entity, aoe: i32 }, + ShowRemoveCurse, ActionWithDirection { function: fn(i: i32, j: i32, ecs: &mut World) -> RunState }, MainMenu { menu_selection: gui::MainMenuSelection }, CharacterCreation { ancestry: gui::Ancestry, class: gui::Class }, @@ -261,6 +262,7 @@ impl GameState for State { RunState::MagicMapReveal { row, cursed } => { new_runstate = RunState::MagicMapReveal { row: row, cursed: cursed } } + RunState::ShowRemoveCurse => new_runstate = RunState::ShowRemoveCurse, _ => new_runstate = RunState::Ticking, } } @@ -380,6 +382,21 @@ impl GameState for State { } } } + RunState::ShowRemoveCurse => { + let result = gui::remove_curse(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => new_runstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + self.ecs + .write_storage::() + .insert(item_entity, Beatitude { buc: BUC::Uncursed, known: true }) + .expect("Unable to insert beatitude"); + new_runstate = RunState::Ticking; + } + } + } RunState::ActionWithDirection { function } => { new_runstate = gui::get_input_direction(&mut self.ecs, ctx, function); }