diff --git a/raws/items.json b/raws/items.json index 2c0c678..7599afe 100644 --- a/raws/items.json +++ b/raws/items.json @@ -3,9 +3,10 @@ "id": "potion_health", "name": { "name": "potion of health", "plural": "potions of health" }, "renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 }, + "class": "potion", "weight": 1, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "heal": "4d4+2" }, "magic": { "class": "uncommon", "naming": "potion" } }, @@ -13,9 +14,10 @@ "id": "potion_health_weak", "name": { "name": "potion of lesser health", "plural": "potions of lesser health" }, "renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 }, + "class": "potion", "weight": 1, "value": 25, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "heal": "2d4+2" }, "magic": { "class": "uncommon", "naming": "potion" } }, @@ -23,27 +25,30 @@ "id": "scroll_identify", "name": { "name": "scroll of identify", "plural": "scrolls of identify" }, "renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 100, - "flags": ["CONSUMABLE", "DESTRUCTIBLE", "IDENTIFY"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "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 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE", "REMOVE_CURSE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "REMOVE_CURSE"], "magic": { "class": "rare", "naming": "scroll" } }, { "id": "scroll_health", "name": { "name": "scroll of healing word", "plural": "scrolls of healing word" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_line": "*;-;#53f06d;75.0;#f9ff9f;100.0", "ranged": "12", "heal": "1d4+2" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -51,9 +56,10 @@ "id": "scroll_mass_health", "name": { "name": "scroll of mass healing word", "plural": "scrolls of mass healing word" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle": "*;#53f06d;200.0", "ranged": "12", "aoe": "3", "heal": "1d4+2" }, "magic": { "class": "rare", "naming": "scroll" } }, @@ -61,9 +67,10 @@ "id": "scroll_magicmissile", "name": { "name": "scroll of magic missile", "plural": "scrolls of magic missile" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_line": "*;-;#00b7ff;75.0;#f4fc83;100.0", "ranged": "12", "damage": "3d4+3;magic" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -71,9 +78,10 @@ "id": "scroll_embers", "name": { "name": "scroll of embers", "plural": "scrolls of embers" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 100, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6;fire", "aoe": "2" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -81,9 +89,10 @@ "id": "scroll_fireball", "name": { "name": "scroll of fireball", "plural": "scrolls of fireball" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_burst": "▓;*;~;#FFA500;#000000;500.0;#ffd381;60.0", "ranged": "10", @@ -96,9 +105,10 @@ "id": "scroll_confusion", "name": { "name": "scroll of confusion", "plural": "scrolls of confusion" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 100, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_line": "*;-;#ad56a6;75.0;#cacaca;100.0", "ranged": "10", "confusion": "4" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -106,9 +116,10 @@ "id": "scroll_mass_confusion", "name": { "name": "scroll of mass confusion", "plural": "scrolls of mass confusion" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle": "*;#ad56a6;200.0", "ranged": "10", "aoe": "3", "confusion": "3" }, "magic": { "class": "veryrare", "naming": "scroll" } }, @@ -116,9 +127,10 @@ "id": "scroll_magicmap", "name": { "name": "scroll of magic mapping", "plural": "scrolls of magic mapping" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE", "MAGICMAP"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "MAGICMAP"], "effects": {}, "magic": { "class": "common", "naming": "scroll" } }, @@ -126,6 +138,7 @@ "id": "equip_dagger", "name": { "name": "dagger", "plural": "daggers" }, "renderable": { "glyph": ")", "fg": "#808080", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 1, "value": 2, "flags": ["EQUIP_MELEE"], @@ -135,6 +148,7 @@ "id": "equip_shortsword", "name": { "name": "shortsword", "plural": "shortswords" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 10, "flags": ["EQUIP_MELEE"], @@ -144,6 +158,7 @@ "id": "equip_rapier", "name": { "name": "rapier", "plural": "rapiers" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 10, "flags": ["EQUIP_MELEE"], @@ -153,6 +168,7 @@ "id": "equip_pitchfork", "name": { "name": "pitchfork", "plural": "pitchforks" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 5, "flags": ["EQUIP_MELEE"], @@ -162,6 +178,7 @@ "id": "equip_sickle", "name": { "name": "sickle", "plural": "sickles" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 5, "flags": ["EQUIP_MELEE"], @@ -171,6 +188,7 @@ "id": "equip_handaxe", "name": { "name": "handaxe", "plural": "handaxes" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 5, "flags": ["EQUIP_MELEE"], @@ -180,6 +198,7 @@ "id": "equip_longsword", "name": { "name": "longsword", "plural": "longswords" }, "renderable": { "glyph": ")", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 3, "value": 15, "flags": ["EQUIP_MELEE"], @@ -189,6 +208,7 @@ "id": "equip_smallshield", "name": { "name": "buckler", "plural": "bucklers" }, "renderable": { "glyph": "[", "fg": "#808080", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 5, "flags": ["EQUIP_SHIELD"], @@ -198,6 +218,7 @@ "id": "equip_mediumshield", "name": { "name": "medium shield", "plural": "medium shields" }, "renderable": { "glyph": "[", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 6, "value": 10, "flags": ["EQUIP_SHIELD"], @@ -207,6 +228,7 @@ "id": "equip_largeshield", "name": { "name": "large shield", "plural": "large shields" }, "renderable": { "glyph": "[", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 12, "value": 35, "flags": ["EQUIP_SHIELD"], @@ -216,6 +238,7 @@ "id": "equip_body_weakleather", "name": { "name": "leather jacket", "plural": "leather jackets" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 8, "value": 5, "flags": ["EQUIP_BODY"], @@ -225,6 +248,7 @@ "id": "equip_body_leather", "name": { "name": "leather chestpiece", "plural": "leather chestpiece" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 10, "value": 10, "flags": ["EQUIP_BODY"], @@ -234,6 +258,7 @@ "id": "equip_body_studdedleather", "name": { "name": "studded leather chestpiece", "plural": "studded leather chestpieces" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 13, "value": 45, "flags": ["EQUIP_BODY"], @@ -243,6 +268,7 @@ "id": "equip_body_ringmail_o", "name": { "name": "orcish ring mail", "plural": "orcish ring mail" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 45, "value": 50, "flags": ["EQUIP_BODY"], @@ -252,6 +278,7 @@ "id": "equip_body_ringmail", "name": { "name": "ring mail", "plural": "ring mail" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 45, "value": 70, "flags": ["EQUIP_BODY"], @@ -261,6 +288,7 @@ "id": "equip_head_leather", "name": { "name": "leather cap", "plural": "leather caps" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 10, "flags": ["EQUIP_HEAD"], @@ -270,6 +298,7 @@ "id": "equip_head_elvish", "name": { "name": "elvish leather helm", "plural": "elvish leather helms" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 25, "flags": ["EQUIP_HEAD"], @@ -279,6 +308,7 @@ "id": "equip_head_o", "name": { "name": "orcish helm", "plural": "orcish helm" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 6, "value": 25, "flags": ["EQUIP_HEAD"], @@ -288,6 +318,7 @@ "id": "equip_head_iron", "name": { "name": "iron helm", "plural": "iron helm" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 10, "value": 45, "flags": ["EQUIP_HEAD"], @@ -297,6 +328,7 @@ "id": "equip_feet_leather", "name": { "name": "leather shoes", "plural": "leather shoes" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 10, "flags": ["EQUIP_FEET"] @@ -305,6 +337,7 @@ "id": "equip_feet_elvish", "name": { "name": "elvish leather shoes", "plural": "elvish leather shoes" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 25, "flags": ["EQUIP_FEET"], @@ -314,6 +347,7 @@ "id": "equip_feet_o", "name": { "name": "orcish boots", "plural": "orcish boots" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 6, "value": 25, "flags": ["EQUIP_FEET"], @@ -323,6 +357,7 @@ "id": "equip_feet_iron", "name": { "name": "iron boots", "plural": "iron boots" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 10, "value": 45, "flags": ["EQUIP_FEET"], @@ -332,6 +367,7 @@ "id": "equip_neck_protection", "name": { "name": "amulet of protection", "plural": "amulets of protection" }, "renderable": { "glyph": "\"", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "amulet", "weight": 1, "value": 200, "flags": ["EQUIP_NECK"], @@ -341,6 +377,7 @@ "id": "equip_back_protection", "name": { "name": "cloak of protection", "plural": "cloaks of protection" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 1, "value": 200, "flags": ["EQUIP_BACK"], @@ -350,6 +387,7 @@ "id": "wand_magicmissile", "name": { "name": "wand of magic missile", "plural": "wands of magic missile" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 100, "flags": ["CHARGES"], @@ -360,6 +398,7 @@ "id": "wand_fireball", "name": { "name": "wand of fireball", "plural": "wands of fireball" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 300, "flags": ["CHARGES"], @@ -370,6 +409,7 @@ "id": "wand_confusion", "name": { "name": "wand of confusion", "plural": "wands of confusion" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 200, "flags": ["CHARGES"], @@ -380,6 +420,7 @@ "id": "wand_digging", "name": { "name": "wand of digging", "plural": "wands of digging" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 300, "flags": ["CHARGES", "DIGGER"], @@ -390,16 +431,18 @@ "id": "food_rations", "name": { "name": "rations", "plural": "rations" }, "renderable": { "glyph": "%", "fg": "#FFA07A", "bg": "#000000", "order": 2 }, + "class": "comestible", "weight": 1, "value": 1, - "flags": ["FOOD", "CONSUMABLE"] + "flags": ["FOOD", "CONSUMABLE", "STACKABLE"] }, { "id": "food_apple", "name": { "name": "apple", "plural": "apples" }, "renderable": { "glyph": "%", "fg": "#00FF00", "bg": "#000000", "order": 2 }, + "class": "comestible", "weight": 0.5, "value": 1, - "flags": ["FOOD", "CONSUMABLE"] + "flags": ["FOOD", "CONSUMABLE", "STACKABLE"] } ] diff --git a/src/components.rs b/src/components.rs index 696eab4..45ca1cf 100644 --- a/src/components.rs +++ b/src/components.rs @@ -258,10 +258,40 @@ pub struct Beatitude { pub known: bool, } +#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)] +pub enum ItemType { + Amulet, + Weapon, + Armour, + Comestible, + Scroll, + Spellbook, + Potion, + Ring, + Wand, +} + +impl ItemType { + pub fn string(&self) -> &str { + match self { + ItemType::Amulet => "Amulets", + ItemType::Weapon => "Weapons", + ItemType::Armour => "Armour", + ItemType::Comestible => "Comestibles", + ItemType::Scroll => "Scrolls", + ItemType::Spellbook => "Spellbooks", + ItemType::Potion => "Potions", + ItemType::Ring => "Rings", + ItemType::Wand => "Wands", + } + } +} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Item { pub weight: f32, // in lbs pub value: f32, // base + pub category: ItemType, } #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] @@ -618,3 +648,20 @@ pub struct EntityMoved {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct MultiAttack {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Stackable {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToRemoveKey {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToDelete {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Key { + pub idx: usize, +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToAssignKey {} diff --git a/src/damage_system.rs b/src/damage_system.rs index a4224a7..b0cc566 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -11,6 +11,8 @@ use super::{ Position, Renderable, RunState, + WantsToRemoveKey, + WantsToDelete, }; use bracket_lib::prelude::*; use specs::prelude::*; @@ -65,7 +67,17 @@ pub fn delete_the_dead(ecs: &mut World) { } } } - let (items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead); + let (mut items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead); + { + let entities = ecs.entities(); + let removekeys = ecs.read_storage::(); + let delete = ecs.read_storage::(); + // Add items marked for deletion to the list, but only if they've already had their + // key assignments handled, to ensurew we don't leave any dangling references behind. + for (e, _d, _r) in (&entities, &delete, !&removekeys).join() { + items_to_delete.push(e); + } + } for loot in loot_to_spawn { crate::raws::spawn_named_entity( &crate::raws::RAWS.lock().unwrap(), @@ -82,6 +94,7 @@ pub fn delete_the_dead(ecs: &mut World) { // For everything that died, increment the event log, and delete. for victim in dead { gamelog::record_event(events::EVENT::Turn(1)); + // TODO: Delete stuff from inventory? This should be handled elsewhere. ecs.delete_entity(victim).expect("Unable to delete."); } } diff --git a/src/data/messages.rs b/src/data/messages.rs index 89e39c8..7175b2a 100644 --- a/src/data/messages.rs +++ b/src/data/messages.rs @@ -25,6 +25,7 @@ pub const NUTRITION_BLESSED: &str = "Delicious"; pub const LEVELUP_PLAYER: &str = "Welcome to experience level"; pub const YOU_PICKUP_ITEM: &str = "You pick up the"; +pub const NO_MORE_KEYS: &str = "Your backpack cannot accomodate any more items"; pub const YOU_DROP_ITEM: &str = "You drop the"; pub const YOU_EQUIP_ITEM: &str = "You equip the"; pub const YOU_REMOVE_ITEM: &str = "You unequip your"; diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index a82496c..fb54eed 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -33,6 +33,8 @@ use crate::{ KnownSpells, Position, Viewshed, + WantsToRemoveKey, + WantsToDelete, }; use crate::data::messages::*; use bracket_lib::prelude::*; @@ -57,7 +59,10 @@ pub fn item_trigger(source: Option, item: Entity, target: &Targets, ecs: let did_something = event_trigger(source, item, target, ecs); // If it's a consumable, delete it if did_something && ecs.read_storage::().get(item).is_some() { - ecs.entities().delete(item).expect("Failed to delete item"); + let mut removekey = ecs.write_storage::(); + removekey.insert(item, WantsToRemoveKey {}).expect("Unable to insert WantsToRemoveKey"); + let mut delete = ecs.write_storage::(); + delete.insert(item, WantsToDelete {}).expect("Unable to insert WantsToDelete"); } } diff --git a/src/gui/identify_menu.rs b/src/gui/identify_menu.rs index 14e0686..422b2a5 100644 --- a/src/gui/identify_menu.rs +++ b/src/gui/identify_menu.rs @@ -3,7 +3,10 @@ use super::{ item_colour_ecs, obfuscate_name_ecs, print_options, + unique_ecs, + check_key, renderable_colour, + letter_to_option, ItemMenuResult, UniqueInventoryItem, BUC, @@ -19,11 +22,12 @@ use crate::{ Name, ObfuscatedName, Renderable, + Key, states::state::*, }; use bracket_lib::prelude::*; use specs::prelude::*; -use std::collections::BTreeMap; +use std::collections::HashMap; /// Handles the Identify menu. pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { @@ -37,38 +41,41 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option(); let renderables = gs.ecs.read_storage::(); let beatitudes = gs.ecs.read_storage::(); + let keys = 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; + (&entities, &items, &renderables, &names, &keys) + .join() + .filter(|(item_entity, _i, _r, n, _k)| { + // 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 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)) && - beatitudes - .get(*item_entity) - .map(|beatitude| beatitude.known) - .unwrap_or(true) - { - return false; - } - return 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)) && + beatitudes + .get(*item_entity) + .map(|beatitude| beatitude.known) + .unwrap_or(true) + { + return false; + } + return true; + }) }; // Build list of items to display @@ -91,34 +98,15 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option().get(entity) - { - match beatitude.buc { - BUC::Blessed => 1, - BUC::Uncursed => 2, - BUC::Cursed => 3, - } - } else { - 0 - }; - let unique_item = 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, - beatitude_status: beatitude_status, - name: name.name.clone(), - }; + let mut player_inventory: super::PlayerInventory = HashMap::new(); + for (entity, _i, renderable, name, key) in build_identify_iterator() { + let unique_item = unique_ecs(&gs.ecs, entity); player_inventory .entry(unique_item) - .and_modify(|(_e, count)| { - *count += 1; + .and_modify(|slot| { + slot.count += 1; }) - .or_insert((entity, 1)); + .or_insert(super::InventorySlot { item: entity, count: 1, idx: key.idx }); } // Get display args let width = get_max_inventory_width(&player_inventory); @@ -133,7 +121,7 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option (ItemMenuResult::NoResponse, None), @@ -141,21 +129,17 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { - let item = player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.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(item)); + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } (ItemMenuResult::NoResponse, None) } diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 13e32d0..73c70f5 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -32,6 +32,9 @@ use super::{ Skills, Viewshed, BUC, + Key, + Item, + ItemType, data::ids::get_local_col, }; use crate::data::entity::CARRY_CAPACITY_PER_STRENGTH; @@ -43,7 +46,9 @@ use crate::data::visuals::{ }; use bracket_lib::prelude::*; use specs::prelude::*; -use std::collections::BTreeMap; +use std::collections::{ HashMap, HashSet }; +use crate::invkeys::check_key; + mod character_creation; mod cheat_menu; mod letter_to_option; @@ -271,41 +276,16 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { ctx.print_color(20, 20, RGB::named(YELLOW), RGB::named(BLACK), "--- GODMODE: ON ---"); } // Draw equipment - let renderables = ecs.read_storage::(); - let mut equipment: Vec<(String, RGB, RGB, FontCharType)> = Vec::new(); - let entities = ecs.entities(); - for (entity, _equipped, renderable) in (&entities, &equipped, &renderables) - .join() - .filter(|item| item.1.owner == *player_entity) { - equipment.push(( - obfuscate_name_ecs(ecs, entity).0, - RGB::named(item_colour_ecs(ecs, entity)), - renderable.fg, - renderable.glyph, - )); - } let mut y = 1; + let equipment = items(&ecs, Filter::Equipped); if !equipment.is_empty() { ctx.print_color(72, y, RGB::named(BLACK), RGB::named(WHITE), "Equipment"); - let mut j = 0; - for item in equipment { - y += 1; - ctx.set(72, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType)); - j += 1; - ctx.set(74, y, item.2, RGB::named(BLACK), item.3); - ctx.print_color(76, y, item.1, RGB::named(BLACK), &item.0); - ctx.print_color( - 76 + &item.0.len() + 1, - y, - RGB::named(WHITE), - RGB::named(BLACK), - "(worn)" - ); - } - y += 2; + y += 1; + y = print_options(&ecs, &equipment, 72, y, ctx); + y += 1; } - // Draw consumables + // Draw backpack ctx.print_color(72, y, RGB::named(BLACK), RGB::named(WHITE), "Backpack"); ctx.print_color( 81, @@ -320,8 +300,8 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { ) ); y += 1; - let player_inventory = get_player_inventory(&ecs); - y = print_options(&player_inventory, 72, y, ctx).0; + let backpack = items(&ecs, Filter::Backpack); + y = print_options(&ecs, &backpack, 72, y, ctx); // Draw spells - if we have any -- NYI! if let Some(known_spells) = ecs.read_storage::().get(*player_entity) { @@ -505,46 +485,46 @@ pub enum ItemMenuResult { } pub fn print_options( + ecs: &World, inventory: &PlayerInventory, mut x: i32, mut y: i32, ctx: &mut BTerm -) -> (i32, i32) { - let mut j = 0; +) -> i32 { let initial_x: i32 = x; - let mut width: i32 = -1; - for (item, (_e, item_count)) in inventory { + let mut sorted: Vec<_> = inventory.iter().collect(); + sorted.sort_by(|a, b| a.1.idx.cmp(&b.1.idx)); + + for (info, slot) in sorted { x = initial_x; // Print the character required to access this item. i.e. (a) - if j < 26 { - ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType)); + if slot.idx < 26 { + ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + slot.idx); } else { // If we somehow have more than 26, start using capitals - ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 65 - 26 + (j as FontCharType)); + ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 65 - 26 + slot.idx); } x += 2; - let fg = RGB::from_u8(item.renderables.0, item.renderables.1, item.renderables.2); - ctx.set(x, y, fg, RGB::named(BLACK), item.glyph); + let fg = RGB::from_u8(info.renderables.0, info.renderables.1, info.renderables.2); + ctx.set(x, y, fg, RGB::named(BLACK), info.glyph); x += 2; - let fg = RGB::from_u8(item.rgb.0, item.rgb.1, item.rgb.2); - if item_count > &1 { + let fg = RGB::from_u8(info.rgb.0, info.rgb.1, info.rgb.2); + if slot.count > 1 { // If more than one, print the number and pluralise // i.e. (a) 3 daggers - ctx.print_color(x, y, fg, RGB::named(BLACK), item_count); + ctx.print_color(x, y, fg, RGB::named(BLACK), slot.count); x += 2; - ctx.print_color(x, y, fg, RGB::named(BLACK), item.display_name.plural.to_string()); - let this_width = x - initial_x + (item.display_name.plural.len() as i32); - width = if width > this_width { width } else { this_width }; + ctx.print_color(x, y, fg, RGB::named(BLACK), info.display_name.plural.to_string()); } else { - if item.display_name.singular.to_lowercase().ends_with("s") { + if info.display_name.singular.to_lowercase().ends_with("s") { ctx.print_color(x, y, fg, RGB::named(BLACK), "some"); x += 5; } else if ['a', 'e', 'i', 'o', 'u'] .iter() - .any(|&v| item.display_name.singular.to_lowercase().starts_with(v)) + .any(|&v| info.display_name.singular.to_lowercase().starts_with(v)) { // If one and starts with a vowel, print 'an' // i.e. (a) an apple @@ -556,40 +536,54 @@ pub fn print_options( ctx.print_color(x, y, fg, RGB::named(BLACK), "a"); x += 2; } - ctx.print_color(x, y, fg, RGB::named(BLACK), item.display_name.singular.to_string()); - let this_width = x - initial_x + (item.display_name.singular.len() as i32); - width = if width > this_width { width } else { this_width }; + /* + let text = if let Some(worn) = ecs.read_storage::().get(slot.item) { + use crate::EquipmentSlot; + let text = match worn.slot { + EquipmentSlot::Melee | EquipmentSlot::Shield => "being held", + _ => "being worn", + }; + format!("{} ({})", info.display_name.singular.to_string(), text) + } else { + info.display_name.singular.to_string() + }; + */ + let text = info.display_name.singular.to_string(); + ctx.print_color(x, y, fg, RGB::named(BLACK), text); } y += 1; - j += 1; } - return (y, width); + return y; } +const PADDING: i32 = 4; +const SOME: i32 = 4; +const AN: i32 = 2; +const A: i32 = 1; + pub fn get_max_inventory_width(inventory: &PlayerInventory) -> i32 { let mut width: i32 = 0; - for (item, (_e, count)) in inventory { + for (item, slot) in inventory { let mut this_width = item.display_name.singular.len() as i32; - // 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 slot.count <= 1 { if item.display_name.singular == item.display_name.plural { - this_width += 4; // "some".len + this_width += SOME; } else if ['a', 'e', 'i', 'o', 'u'].iter().any(|&v| item.display_name.singular.starts_with(v)) { - this_width += 2; // "an".len + this_width += AN; } else { - this_width += 1; // "a".len + this_width += A; } } else { - this_width += count.to_string().len() as i32; // i.e. "12".len + this_width = + (item.display_name.plural.len() as i32) + (slot.count.to_string().len() as i32); // i.e. "12".len } width = if width > this_width { width } else { this_width }; } - return width; + return width + PADDING; } // Inside the ECS @@ -636,7 +630,7 @@ pub fn obfuscate_name( if has_beatitude.known { let prefix = match has_beatitude.buc { BUC::Cursed => Some("cursed "), - BUC::Uncursed => None, + BUC::Uncursed => Some("uncursed "), BUC::Blessed => Some("blessed "), }; if prefix.is_some() { @@ -831,13 +825,13 @@ pub fn show_help(ctx: &mut BTerm) -> YesNoResult { } } -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] struct DisplayName { singular: String, plural: String, } -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct UniqueInventoryItem { display_name: DisplayName, rgb: (u8, u8, u8), @@ -847,57 +841,71 @@ pub struct UniqueInventoryItem { name: String, } -pub type PlayerInventory = BTreeMap; +pub struct InventorySlot { + pub item: Entity, + pub count: i32, + pub idx: usize, +} -pub fn get_player_inventory(ecs: &World) -> PlayerInventory { - let player_entity = ecs.fetch::(); - let names = ecs.read_storage::(); - let backpack = ecs.read_storage::(); - let entities = ecs.entities(); - let renderables = ecs.read_storage::(); +pub type PlayerInventory = HashMap; - let mut player_inventory: BTreeMap = BTreeMap::new(); - for (entity, _pack, name, renderable) in (&entities, &backpack, &names, &renderables) - .join() - .filter(|item| item.1.owner == *player_entity) { - // RGB can't be used as a key. This is converting the RGB (tuple of f32) into a tuple of u8s. - let item_colour = item_colour_ecs(ecs, entity); - let renderables = ( - (renderable.fg.r * 255.0) as u8, - (renderable.fg.g * 255.0) as u8, - (renderable.fg.b * 255.0) as u8, - ); - let (singular, plural) = obfuscate_name_ecs(ecs, entity); - let beatitude_status = if let Some(beatitude) = ecs.read_storage::().get(entity) { - match beatitude.buc { - BUC::Blessed => 1, - BUC::Uncursed => 2, - BUC::Cursed => 3, - } - } else { - 0 - }; - let unique_item = UniqueInventoryItem { - display_name: DisplayName { singular: singular.clone(), plural: plural }, - rgb: item_colour, - renderables: renderables, - glyph: renderable.glyph, - beatitude_status: beatitude_status, - name: name.name.clone(), - }; - player_inventory - .entry(unique_item) - .and_modify(|(_e, count)| { - *count += 1; +pub enum Filter { + All, + Backpack, + Equipped, + Category(ItemType), +} + +macro_rules! includeitem { + ($inv:expr, $ecs:expr, $e:expr, $k:expr) => { + $inv.entry(unique_ecs($ecs, $e)) + .and_modify(|slot| { + slot.count += 1; }) - .or_insert((entity, 1)); - } + .or_insert(InventorySlot { + item: $e, + count: 1, + idx: $k.idx, + }); + }; +} - return player_inventory; +pub fn items(ecs: &World, filter: Filter) -> PlayerInventory { + let entities = ecs.entities(); + let keys = ecs.read_storage::(); + let mut inv: PlayerInventory = HashMap::new(); + match filter { + Filter::All => { + for (e, k) in (&entities, &keys).join() { + includeitem!(inv, ecs, e, k); + } + } + Filter::Backpack => { + let backpack = ecs.read_storage::(); + for (e, k, _b) in (&entities, &keys, &backpack).join() { + includeitem!(inv, ecs, e, k); + } + } + Filter::Equipped => { + let equipped = ecs.read_storage::(); + for (e, k, _e) in (&entities, &keys, &equipped).join() { + includeitem!(inv, ecs, e, k); + } + } + Filter::Category(itemtype) => { + let items = ecs.read_storage::(); + for (e, k, _i) in (&entities, &keys, &items) + .join() + .filter(|e| e.2.category == itemtype) { + includeitem!(inv, ecs, e, k); + } + } + } + inv } pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { - let player_inventory = get_player_inventory(&gs.ecs); + let player_inventory = items(&gs.ecs, Filter::Backpack); let count = player_inventory.len(); let (x_offset, y_offset) = (1, 10); @@ -915,7 +923,7 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio let y = 3 + y_offset; let width = get_max_inventory_width(&player_inventory); ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); - print_options(&player_inventory, x + 1, y + 1, ctx); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -924,22 +932,23 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { let selection = letter_to_option::letter_to_option(key, ctx.shift); - if selection > -1 && selection < (count as i32) { + if selection != -1 && check_key(selection as usize) { if on_overmap { gamelog::Logger ::new() .append("You can't use items on the overmap.") .log(); } else { - return ( - ItemMenuResult::Selected, - Some( - player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.0 - ), - ); + // Get the first entity with a Key {} component that has idx matching selection + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } + // TODO: Gamelog about not having selected item? } } (ItemMenuResult::NoResponse, None) @@ -949,7 +958,7 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio } pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { - let player_inventory = get_player_inventory(&gs.ecs); + let player_inventory = items(&gs.ecs, Filter::Backpack); let count = player_inventory.len(); let (x_offset, y_offset) = (1, 10); @@ -967,7 +976,7 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio let y = 3 + y_offset; let width = get_max_inventory_width(&player_inventory); ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); - print_options(&player_inventory, x + 1, y + 1, ctx); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -975,23 +984,23 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio match key { VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { if on_overmap { gamelog::Logger ::new() .append("You can't drop items on the overmap.") .log(); } else { - return ( - ItemMenuResult::Selected, - Some( - player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.0 - ), - ); + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } } (ItemMenuResult::NoResponse, None) @@ -1001,11 +1010,8 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio } pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { - let player_entity = gs.ecs.fetch::(); - let backpack = gs.ecs.read_storage::(); - let entities = gs.ecs.entities(); - let inventory = (&backpack).join().filter(|item| item.owner == *player_entity); - let count = inventory.count(); + let player_inventory = items(&gs.ecs, Filter::Equipped); + let count = player_inventory.len(); let (x_offset, y_offset) = (1, 10); @@ -1017,38 +1023,11 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Opt "Unequip what? [aA-zZ][Esc.]" ); - let mut equippable: Vec<(Entity, String)> = Vec::new(); - let mut width = 2; - for (entity, _pack) in (&entities, &backpack) - .join() - .filter(|item| item.1.owner == *player_entity) { - let this_name = &obfuscate_name_ecs(&gs.ecs, entity).0; - let this_width = 5 + this_name.len(); - width = if width > this_width { width } else { this_width }; - equippable.push((entity, this_name.to_string())); - } - let x = 1 + x_offset; - let mut y = 3 + y_offset; - - ctx.draw_box(x, y, width, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); - y += 1; - - let mut j = 0; - let renderables = gs.ecs.read_storage::(); - for (e, name) in &equippable { - let (mut fg, glyph) = if let Some(renderable) = renderables.get(*e) { - (renderable.fg, renderable.glyph) - } else { - (RGB::named(WHITE), to_cp437('-')) - }; - ctx.set(x + 1, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType)); - ctx.set(x + 3, y, fg, RGB::named(BLACK), glyph); - fg = RGB::named(item_colour_ecs(&gs.ecs, *e)); - ctx.print_color(x + 5, y, fg, RGB::named(BLACK), name); - y += 1; - j += 1; - } + let y = 3 + y_offset; + let width = get_max_inventory_width(&player_inventory); + ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -1056,9 +1035,17 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Opt match key { VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { - return (ItemMenuResult::Selected, Some(equippable[selection as usize].0)); + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let equipped = gs.ecs.read_storage::(); + for (e, key, _e) in (&entities, &keyed_items, &equipped).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } (ItemMenuResult::NoResponse, None) } @@ -1458,3 +1445,72 @@ pub fn with_article(name: String) -> String { } format!("a {}", name) } + +pub fn unique( + entity: Entity, + names: &ReadStorage, + obfuscated_names: &ReadStorage, + renderables: &ReadStorage, + beatitudes: &ReadStorage, + magic_items: &ReadStorage, + charges: Option<&ReadStorage>, + dm: &MasterDungeonMap +) -> UniqueInventoryItem { + let item_colour = item_colour(entity, beatitudes); + let (singular, plural) = obfuscate_name( + entity, + names, + magic_items, + obfuscated_names, + beatitudes, + dm, + charges + ); + let (renderables, glyph) = if let Some(renderable) = renderables.get(entity) { + ( + ( + (renderable.fg.r * 255.0) as u8, + (renderable.fg.g * 255.0) as u8, + (renderable.fg.b * 255.0) as u8, + ), + renderable.glyph, + ) + } else { + unreachable!("Item has no renderable component.") + }; + let name = if let Some(name) = names.get(entity) { + name + } else { + unreachable!("Item has no name component.") + }; + let beatitude_status = if let Some(beatitude) = beatitudes.get(entity) { + match beatitude.buc { + BUC::Blessed => 1, + BUC::Uncursed => 2, + BUC::Cursed => 3, + } + } else { + 0 + }; + UniqueInventoryItem { + display_name: DisplayName { singular: singular.clone(), plural }, + rgb: item_colour, + renderables, + glyph, + beatitude_status, + name: name.name.clone(), + } +} + +pub fn unique_ecs(ecs: &World, entity: Entity) -> UniqueInventoryItem { + return unique( + entity, + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + Some(&ecs.read_storage::()), + &ecs.fetch::() + ); +} diff --git a/src/gui/remove_curse_menu.rs b/src/gui/remove_curse_menu.rs index f8d1f14..247fd41 100644 --- a/src/gui/remove_curse_menu.rs +++ b/src/gui/remove_curse_menu.rs @@ -3,9 +3,13 @@ use super::{ item_colour_ecs, obfuscate_name_ecs, print_options, + unique_ecs, renderable_colour, + check_key, + letter_to_option, ItemMenuResult, UniqueInventoryItem, + InventorySlot, }; use crate::{ gamelog, @@ -18,10 +22,11 @@ use crate::{ Renderable, states::state::*, BUC, + Key, }; use bracket_lib::prelude::*; use specs::prelude::*; -use std::collections::BTreeMap; +use std::collections::HashMap; /// Handles the Remove Curse menu. pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { @@ -33,11 +38,12 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< let beatitudes = gs.ecs.read_storage::(); let names = gs.ecs.read_storage::(); let renderables = gs.ecs.read_storage::(); + let keys = gs.ecs.read_storage::(); let build_cursed_iterator = || { - (&entities, &items, &beatitudes, &renderables, &names) + (&entities, &items, &beatitudes, &renderables, &names, &keys) .join() - .filter(|(item_entity, _i, b, _r, _n)| { + .filter(|(item_entity, _i, b, _r, _n, _k)| { // Set all items to FALSE initially. let mut keep = false; // If found in the player's backpack, set to TRUE @@ -86,8 +92,8 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< .log(); return (ItemMenuResult::Selected, Some(item)); } - let mut player_inventory: super::PlayerInventory = BTreeMap::new(); - for (entity, _i, _b, renderable, name) in build_cursed_iterator() { + let mut player_inventory: super::PlayerInventory = HashMap::new(); + for (entity, _i, _b, renderable, name, key) in build_cursed_iterator() { let (singular, plural) = obfuscate_name_ecs(&gs.ecs, entity); let beatitude_status = if let Some(beatitude) = gs.ecs.read_storage::().get(entity) @@ -100,20 +106,17 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< } else { 0 }; - let unique_item = 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, - beatitude_status: beatitude_status, - name: name.name.clone(), - }; + let unique_item = unique_ecs(&gs.ecs, entity); player_inventory .entry(unique_item) - .and_modify(|(_e, count)| { - *count += 1; + .and_modify(|slot| { + slot.count += 1; }) - .or_insert((entity, 1)); + .or_insert(InventorySlot { + item: entity, + count: 1, + idx: key.idx, + }); } // Get display args let width = get_max_inventory_width(&player_inventory); @@ -128,7 +131,7 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< "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); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); // Input match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -136,21 +139,17 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< match key { VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { - let item = player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.0; - 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)); + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } (ItemMenuResult::NoResponse, None) } diff --git a/src/inventory/collection_system.rs b/src/inventory/collection_system.rs index 2fb7276..5ef9619 100644 --- a/src/inventory/collection_system.rs +++ b/src/inventory/collection_system.rs @@ -12,6 +12,9 @@ use crate::{ ObfuscatedName, Position, WantsToPickupItem, + WantsToAssignKey, + Renderable, + Stackable, }; use specs::prelude::*; use crate::data::messages; @@ -30,9 +33,12 @@ impl<'a> System<'a> for ItemCollectionSystem { WriteStorage<'a, EquipmentChanged>, ReadStorage<'a, MagicItem>, ReadStorage<'a, ObfuscatedName>, + ReadStorage<'a, Renderable>, ReadStorage<'a, Beatitude>, ReadExpect<'a, MasterDungeonMap>, ReadStorage<'a, Charges>, + ReadStorage<'a, WantsToAssignKey>, + ReadStorage<'a, Stackable>, ); fn run(&mut self, data: Self::SystemData) { @@ -45,20 +51,16 @@ impl<'a> System<'a> for ItemCollectionSystem { mut equipment_changed, magic_items, obfuscated_names, + renderables, beatitudes, dm, wands, + wants_key, + stackable, ) = data; - - for pickup in wants_pickup.join() { - positions.remove(pickup.item); - backpack - .insert(pickup.item, InBackpack { owner: pickup.collected_by }) - .expect("Unable to pickup item."); - equipment_changed - .insert(pickup.collected_by, EquipmentChanged {}) - .expect("Unable to insert EquipmentChanged."); - + let mut to_remove: Vec = Vec::new(); + // For every item that wants to be picked up that *isn't* waiting on a key assignment. + for (pickup, _key) in (&wants_pickup, !&wants_key).join() { if pickup.collected_by == *player_entity { gamelog::Logger ::new() @@ -82,8 +84,17 @@ impl<'a> System<'a> for ItemCollectionSystem { .period() .log(); } + positions.remove(pickup.item); + backpack + .insert(pickup.item, InBackpack { owner: pickup.collected_by }) + .expect("Unable to pickup item"); + equipment_changed + .insert(pickup.collected_by, EquipmentChanged {}) + .expect("Unable to insert EquipmentChanged"); + to_remove.push(pickup.collected_by); + } + for item in to_remove.iter() { + wants_pickup.remove(*item); } - - wants_pickup.clear(); } } diff --git a/src/inventory/drop_system.rs b/src/inventory/drop_system.rs index 34084b4..af2d8a2 100644 --- a/src/inventory/drop_system.rs +++ b/src/inventory/drop_system.rs @@ -12,6 +12,7 @@ use crate::{ ObfuscatedName, Position, WantsToDropItem, + WantsToRemoveKey, }; use specs::prelude::*; use crate::data::messages; @@ -34,6 +35,7 @@ impl<'a> System<'a> for ItemDropSystem { ReadStorage<'a, ObfuscatedName>, ReadExpect<'a, MasterDungeonMap>, ReadStorage<'a, Charges>, + WriteStorage<'a, WantsToRemoveKey>, ); fn run(&mut self, data: Self::SystemData) { @@ -50,6 +52,7 @@ impl<'a> System<'a> for ItemDropSystem { obfuscated_names, dm, wands, + mut keys, ) = data; for (entity, to_drop) in (&entities, &wants_drop).join() { @@ -68,6 +71,9 @@ impl<'a> System<'a> for ItemDropSystem { backpack.remove(to_drop.item); if entity == *player_entity { + keys.insert(to_drop.item, WantsToRemoveKey {}).expect( + "Unable to insert WantsToRemoveKey" + ); gamelog::Logger ::new() .append(messages::YOU_DROP_ITEM) diff --git a/src/inventory/keyhandling.rs b/src/inventory/keyhandling.rs new file mode 100644 index 0000000..7194a18 --- /dev/null +++ b/src/inventory/keyhandling.rs @@ -0,0 +1,153 @@ +use crate::{ + gamelog, + gui::unique, + Beatitude, + Charges, + MagicItem, + MasterDungeonMap, + Name, + ObfuscatedName, + Stackable, + Renderable, + WantsToAssignKey, + WantsToRemoveKey, + Key, +}; +use specs::prelude::*; +use crate::data::messages; +use bracket_lib::prelude::*; +use crate::invkeys::*; + +pub struct KeyHandling {} + +const DEBUG_KEYHANDLING: bool = true; + +impl<'a> System<'a> for KeyHandling { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + WriteStorage<'a, WantsToAssignKey>, + WriteStorage<'a, WantsToRemoveKey>, + WriteStorage<'a, Key>, + ReadStorage<'a, Stackable>, + ReadStorage<'a, Name>, + ReadStorage<'a, ObfuscatedName>, + ReadStorage<'a, Renderable>, + ReadStorage<'a, Beatitude>, + ReadStorage<'a, MagicItem>, + ReadStorage<'a, Charges>, + ReadExpect<'a, MasterDungeonMap>, + ); + + fn run(&mut self, data: Self::SystemData) { + let ( + entities, + mut wants_keys, + mut wants_removekey, + mut keys, + stackable, + names, + obfuscated_names, + renderables, + beatitudes, + magic_items, + wands, + dm, + ) = data; + + // For every entity that wants to be picked up, that still needs a key assigned. + for (e, _wants_key) in (&entities, &wants_keys).join() { + if DEBUG_KEYHANDLING { + console::log(&format!("KEYHANDLING: Assigning key to {:?}", e)); + } + let (stacks, mut handled, unique) = ( + if let Some(_) = stackable.get(e) { true } else { false }, + false, + unique( + e, + &names, + &obfuscated_names, + &renderables, + &beatitudes, + &magic_items, + Some(&wands), + &dm + ), + ); + if stacks { + console::log(&format!("KEYHANDLING: Item is stackable.")); + let maybe_key = item_exists(&unique); + if maybe_key.is_some() { + console::log(&format!("KEYHANDLING: Existing stack found for this item.")); + let key = maybe_key.unwrap(); + keys.insert(e, Key { idx: key }).expect("Unable to insert Key."); + console::log(&format!("KEYHANDLING: Assigned key idx {} to item.", key)); + handled = true; + } + } + if !handled { + console::log( + &format!("KEYHANDLING: Item is not stackable, or no existing stack found.") + ); + if let Some(idx) = assign_next_available() { + console::log( + &format!("KEYHANDLING: Assigned next available index {} to item.", idx) + ); + keys.insert(e, Key { idx }).expect("Unable to insert Key."); + register_stackable(stacks, unique, idx); + } else { + console::log(&format!("KEYHANDLING: No more keys available.")); + gamelog::Logger + ::new() + .append(messages::NO_MORE_KEYS) + .colour(WHITE) + .period() + .log(); + } + } + } + for (e, _wants_key) in (&entities, &wants_removekey).join() { + let idx = keys.get(e).unwrap().idx; + if DEBUG_KEYHANDLING { + console::log(&format!("KEYHANDLING: Removing key from {:?}", e)); + } + // If the item is *not* stackable, then we can just remove the key and clear the index. + if let None = stackable.get(e) { + console::log( + &format!("KEYHANDLING: Item is not stackable, clearing index {}.", idx) + ); + clear_idx(idx); + keys.remove(e); + continue; + } + // If the item *is* stackable, then we need to check if there are any other items that + // share this key assignment, before clearing the index. + console::log( + &format!( + "KEYHANDLING: Item is stackable, checking if any other items share this key." + ) + ); + let mut sole_item_with_key = true; + for (entity, key) in (&entities, &keys).join() { + if entity != e && key.idx == idx { + console::log(&format!("KEYHANDLING: Another item shares index {}", idx)); + sole_item_with_key = false; + break; + } + } + // If no other items shared this key, free up the index. + if sole_item_with_key { + console::log( + &format!("KEYHANDLING: No other items found, clearing index {}.", idx) + ); + clear_idx(idx); + } + // Either way, remove the key component from this item, because we're dropping it. + console::log(&format!("KEYHANDLING: Removing key component from item.")); + keys.remove(e); + } + + wants_removekey.clear(); + wants_keys.clear(); + } +} diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index eceaccb..76748e0 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -4,6 +4,7 @@ mod equip_system; mod identification_system; mod remove_system; mod use_system; +mod keyhandling; pub use self::{ collection_system::ItemCollectionSystem, @@ -12,4 +13,5 @@ pub use self::{ identification_system::ItemIdentificationSystem, remove_system::ItemRemoveSystem, use_system::ItemUseSystem, + keyhandling::KeyHandling, }; diff --git a/src/invkeys.rs b/src/invkeys.rs new file mode 100644 index 0000000..e52824b --- /dev/null +++ b/src/invkeys.rs @@ -0,0 +1,60 @@ +use std::sync::Mutex; +use std::collections::{ HashMap }; +use specs::prelude::*; +use crate::gui::UniqueInventoryItem; + +lazy_static! { + pub static ref INVKEYS: Mutex> = Mutex::new(HashMap::new()); + pub static ref ASSIGNEDKEYS: Mutex> = Mutex::new(vec![false; 52]); +} + +/// For (de)serialization. +pub fn clone_invkeys() -> HashMap { + let invkeys = INVKEYS.lock().unwrap(); + invkeys.clone() +} +pub fn restore_invkeys(invkeys: HashMap) { + INVKEYS.lock().unwrap().clear(); + INVKEYS.lock().unwrap().extend(invkeys); +} + +pub fn check_key(idx: usize) -> bool { + let lock = ASSIGNEDKEYS.lock().unwrap(); + lock[idx] +} + +pub fn item_exists(item: &UniqueInventoryItem) -> Option { + let invkeys = INVKEYS.lock().unwrap(); + use bracket_lib::prelude::*; + console::log(&format!("{:?}", item)); + if invkeys.contains_key(item) { + Some(*invkeys.get(item).unwrap()) + } else { + None + } +} + +pub fn assign_next_available() -> Option { + let mut lock = ASSIGNEDKEYS.lock().unwrap(); + for (i, key) in lock.iter_mut().enumerate() { + if !*key { + *key = true; + return Some(i); + } + } + None +} + +pub fn register_stackable(stacks: bool, item: UniqueInventoryItem, idx: usize) { + if stacks { + let mut invkeys = INVKEYS.lock().unwrap(); + invkeys.insert(item, idx); + } +} + +pub fn clear_idx(idx: usize) { + let mut lock = ASSIGNEDKEYS.lock().unwrap(); + lock[idx] = false; + let mut invkeys = INVKEYS.lock().unwrap(); + invkeys.retain(|_k, v| *v != idx); +} diff --git a/src/lib.rs b/src/lib.rs index 6bbdd04..e184a58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ pub mod rex_assets; pub mod spatial; pub mod morgue; pub mod states; +pub mod invkeys; pub use components::*; use particle_system::ParticleBuilder; diff --git a/src/main.rs b/src/main.rs index f430376..fc2c72b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -112,6 +112,11 @@ fn main() -> 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::>(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/player.rs b/src/player.rs index dd8ecf3..36af74b 100644 --- a/src/player.rs +++ b/src/player.rs @@ -30,6 +30,7 @@ use super::{ Viewshed, WantsToMelee, WantsToPickupItem, + WantsToAssignKey, get_dest, Destination, DamageType, @@ -633,7 +634,9 @@ fn get_item(ecs: &mut World) -> RunState { return RunState::AwaitingInput; } Some(item) => { + let mut assignkey = ecs.write_storage::(); let mut pickup = ecs.write_storage::(); + assignkey.insert(item, WantsToAssignKey {}).expect("Unable to insert WantsToAssignKey"); pickup .insert(*player_entity, WantsToPickupItem { collected_by: *player_entity, item }) .expect("Unable to insert want to pickup item."); diff --git a/src/raws/item_structs.rs b/src/raws/item_structs.rs index 897d48d..c670e54 100644 --- a/src/raws/item_structs.rs +++ b/src/raws/item_structs.rs @@ -6,6 +6,7 @@ pub struct Item { pub id: String, pub name: Name, pub renderable: Option, + pub class: String, pub weight: Option, pub value: Option, pub equip: Option, diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 813dbe2..0e2b227 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -66,6 +66,7 @@ macro_rules! apply_flags { "IDENTIFY" => $eb = $eb.with(ProvidesIdentify {}), "DIGGER" => $eb = $eb.with(Digger {}), "MAGICMAP" => $eb = $eb.with(MagicMapper {}), + "STACKABLE" => $eb = $eb.with(Stackable {}), // CAN BE DESTROYED BY DAMAGE "DESTRUCTIBLE" => $eb = $eb.with(Destructible {}), // --- EQUIP SLOTS --- @@ -281,6 +282,7 @@ pub fn spawn_named_item( if known_beatitude && !identified_items.contains(&item_template.name.name) { dm.identified_items.insert(item_template.name.name.clone()); } + let needs_key = is_player_owned(&player_entity, &pos); std::mem::drop(player_entity); std::mem::drop(dm); // -- DROP EVERYTHING THAT INVOLVES THE ECS BEFORE THIS POINT --- @@ -293,9 +295,23 @@ pub fn spawn_named_item( eb = eb.with(Item { weight: item_template.weight.unwrap_or(0.0), value: item_template.value.unwrap_or(0.0), + category: match item_template.class.as_str() { + "amulet" => ItemType::Amulet, + "weapon" => ItemType::Weapon, + "armour" => ItemType::Armour, + "comestible" => ItemType::Comestible, + "scroll" => ItemType::Scroll, + "spellbook" => ItemType::Spellbook, + "potion" => ItemType::Potion, + "ring" => ItemType::Ring, + "wand" => ItemType::Wand, + _ => unreachable!("Unknown item type."), + }, }); eb = spawn_position(pos, eb, key, raws); - + if needs_key { + eb = eb.with(WantsToAssignKey {}); + } if let Some(renderable) = &item_template.renderable { eb = eb.with(get_renderable_component(renderable)); } @@ -392,6 +408,7 @@ pub fn spawn_named_mob( if raws.mob_index.contains_key(key) { let mob_template = &raws.raws.mobs[raws.mob_index[key]]; let mut player_level = 1; + let needs_key; { let pools = ecs.read_storage::(); let player_entity = ecs.fetch::(); @@ -399,12 +416,15 @@ pub fn spawn_named_mob( if let Some(pool) = player_pool { player_level = pool.level; } + needs_key = is_player_owned(&player_entity, &pos); } - let mut eb; // New entity with a position, name, combatstats, and viewshed eb = ecs.create_entity().marked::>(); eb = spawn_position(pos, eb, key, raws); + if needs_key { + eb = eb.with(WantsToAssignKey {}); + } eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() }); eb = eb.with(Viewshed { visible_tiles: Vec::new(), @@ -632,10 +652,18 @@ pub fn spawn_named_prop( pos: SpawnType ) -> Option { if raws.prop_index.contains_key(key) { + let needs_key; + { + let player_entity = ecs.fetch::(); + needs_key = is_player_owned(&player_entity, &pos); + } // ENTITY BUILDER PREP let prop_template = &raws.raws.props[raws.prop_index[key]]; let mut eb = ecs.create_entity().marked::>(); eb = spawn_position(pos, eb, key, raws); + if needs_key { + eb = eb.with(WantsToAssignKey {}); + } // APPLY MANDATORY COMPONENTS FOR A PROP: // - Name // - Prop {} @@ -686,6 +714,23 @@ fn spawn_position<'a>( eb } +fn is_player_owned(player: &Entity, pos: &SpawnType) -> bool { + match pos { + SpawnType::Carried { by } => { + if by == player { + return true; + } + } + SpawnType::Equipped { by } => { + if by == player { + return true; + } + } + _ => {} + } + false +} + fn get_renderable_component( renderable: &super::item_structs::Renderable ) -> crate::components::Renderable { diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 3a3f733..f3b284d 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -99,6 +99,7 @@ pub fn save_game(ecs: &mut World) { IntrinsicChanged, Intrinsics, Item, + Key, KnownSpells, LootTable, MagicItem, @@ -128,17 +129,21 @@ pub fn save_game(ecs: &mut World) { SpawnParticleBurst, SpawnParticleLine, SpawnParticleSimple, + Stackable, TakingTurn, Telepath, ToHitBonus, Viewshed, Charges, WantsToApproach, + WantsToAssignKey, + WantsToDelete, WantsToDropItem, WantsToFlee, WantsToMelee, WantsToPickupItem, WantsToRemoveItem, + WantsToRemoveKey, WantsToUseItem, SerializationHelper, DMSerializationHelper @@ -232,6 +237,7 @@ pub fn load_game(ecs: &mut World) { IntrinsicChanged, Intrinsics, Item, + Key, KnownSpells, LootTable, MagicItem, @@ -261,17 +267,21 @@ pub fn load_game(ecs: &mut World) { SpawnParticleBurst, SpawnParticleLine, SpawnParticleSimple, + Stackable, TakingTurn, Telepath, ToHitBonus, Viewshed, Charges, WantsToApproach, + WantsToAssignKey, + WantsToDelete, WantsToDropItem, WantsToFlee, WantsToMelee, WantsToPickupItem, WantsToRemoveItem, + WantsToRemoveKey, WantsToUseItem, SerializationHelper, DMSerializationHelper diff --git a/src/states/state.rs b/src/states/state.rs index 0217cca..3dde32b 100644 --- a/src/states/state.rs +++ b/src/states/state.rs @@ -64,12 +64,13 @@ impl State { fn resolve_entity_decisions(&mut self) { let mut trigger_system = trigger_system::TriggerSystem {}; - let mut inventory_system = inventory::ItemCollectionSystem {}; let mut item_equip_system = inventory::ItemEquipSystem {}; let mut item_use_system = inventory::ItemUseSystem {}; let mut item_drop_system = inventory::ItemDropSystem {}; let mut item_remove_system = inventory::ItemRemoveSystem {}; + let mut inventory_system = inventory::ItemCollectionSystem {}; let mut item_id_system = inventory::ItemIdentificationSystem {}; + let mut key_system = inventory::KeyHandling {}; let mut melee_system = MeleeCombatSystem {}; trigger_system.run_now(&self.ecs); inventory_system.run_now(&self.ecs); @@ -78,6 +79,7 @@ impl State { item_drop_system.run_now(&self.ecs); item_remove_system.run_now(&self.ecs); item_id_system.run_now(&self.ecs); + key_system.run_now(&self.ecs); melee_system.run_now(&self.ecs); effects::run_effects_queue(&mut self.ecs); @@ -342,7 +344,11 @@ impl GameState for State { gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); + let mut removekey = self.ecs.write_storage::(); let mut intent = self.ecs.write_storage::(); + removekey + .insert(item_entity, WantsToRemoveKey {}) + .expect("Unable to insert WantsToRemoveKey"); intent .insert(*self.ecs.fetch::(), WantsToDropItem { item: item_entity,