diff --git a/.github/workflows/cargo-build-test.yml b/.github/workflows/cargo-build-test.yml
index d54fa2f..cafaa58 100644
--- a/.github/workflows/cargo-build-test.yml
+++ b/.github/workflows/cargo-build-test.yml
@@ -12,7 +12,7 @@ env:
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
diff --git a/Cargo.toml b/Cargo.toml
index 24b6918..55d7645 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rust-rl"
-version = "0.1.1"
+version = "0.1.4"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/README.md b/README.md
index 9127091..c1809cf 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,18 @@
#### using _rltk/bracket-lib_, and _specs_
-check out the page in the header for the wasm version, pick [a release of your choice](https://github.com/Llywelwyn/rust-rl/releases), or build manually with:
+[](https://github.com/Llywelwyn/rust-rl/actions/workflows/cargo-build-test.yml)
+
+check out the page in the header for the wasm version, pick [a release](https://github.com/Llywelwyn/rust-rl/releases), or build manually with:
`git clone https://github.com/Llywelwyn/rust-rl/ && cd rust-rl && cargo build --release`,

-this year for roguelikedev does the complete tutorial, i followed along with thebracket's [_roguelike tutorial - in rust_](https://bfnightly.bracketproductions.com). the notes i made during the sprint are being kept below for posterity - further changes since then are noted in [changelog.txt](https://github.com/Llywelwyn/rust-rl/blob/9150ed39e45bee536060cdc769d274e639021012/changelog.txt), and in the release notes.
-
-i'm also working on translating over my progress into blog entries on my site @ [llyw.co.uk](https://llyw.co.uk/), with a larger focus on some of the more interesting implementation details.
-
---
+
+ boring details about the sprint where this project started
week 1
@@ -157,3 +157,4 @@ i'm also working on translating over my progress into blog entries on my site @

+
diff --git a/docs/combat_system.txt b/docs/combat_system.txt
index 13a780b..44e2f65 100644
--- a/docs/combat_system.txt
+++ b/docs/combat_system.txt
@@ -49,3 +49,7 @@ Complex example, with negative AC:
bloodstains: if starts on bloodied tile, remove blood + heal, gain xp, grow (little dog -> dog), etc.
- You have negative AC, so you roll 1d14 for damage reduction, and get an 8.
- The total damage is 6 - 8 = -2, but damage can't be negative, so you take 1 point of damage.
+
+tl;dr
+1. Lower AC is better
+2. Aim for 0 AC - it's an important breakpoint. Every point of AC before 0 counts for a lot.
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/ai/turn_status_system.rs b/src/ai/turn_status_system.rs
index db3acaa..e072e45 100644
--- a/src/ai/turn_status_system.rs
+++ b/src/ai/turn_status_system.rs
@@ -65,9 +65,7 @@ impl<'a> System<'a> for TurnStatusSystem {
not_confused.push(entity);
if entity == *player_entity {
logger = logger
- .colour(renderable_colour(&renderables, entity))
.append("You")
- .colour(WHITE)
.append("snap out of it.");
log = true;
} else {
@@ -94,9 +92,7 @@ impl<'a> System<'a> for TurnStatusSystem {
not_my_turn.push(entity);
if entity == *player_entity {
logger = logger
- .colour(renderable_colour(&renderables, entity))
.append("You")
- .colour(WHITE)
.append("are confused!");
log = true;
gamelog::record_event(EVENT::PlayerConfused(1));
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/config/mod.rs b/src/config/mod.rs
index 5309151..f86c5df 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -34,7 +34,7 @@ impl Default for Config {
fn default() -> Self {
Config {
logging: LogConfig {
- show_mapgen: true,
+ show_mapgen: false,
log_combat: false,
log_spawning: false,
log_ticks: false,
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..cb4e5d3 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");
}
}
@@ -218,9 +223,7 @@ fn handle_healing(
let renderables = ecs.read_storage::();
if ecs.read_storage::().get(target).is_some() {
logger = logger
- .colour(renderable_colour(&renderables, target))
.append("You")
- .colour(WHITE)
.append(HEAL_PLAYER_HIT)
.buc(event.buc.clone(), None, Some(HEAL_PLAYER_HIT_BLESSED));
} else {
@@ -262,9 +265,7 @@ fn handle_damage(
let player_viewshed = viewsheds.get(*ecs.fetch::()).unwrap();
if ecs.read_storage::().get(target).is_some() {
logger = logger
- .colour(renderable_colour(&renderables, target))
.append("You")
- .colour(WHITE)
.append(DAMAGE_PLAYER_HIT);
event.log = true;
} else if
diff --git a/src/gui/identify_menu.rs b/src/gui/identify_menu.rs
index 14e0686..31ce8d7 100644
--- a/src/gui/identify_menu.rs
+++ b/src/gui/identify_menu.rs
@@ -3,10 +3,10 @@ use super::{
item_colour_ecs,
obfuscate_name_ecs,
print_options,
- renderable_colour,
+ unique_ecs,
+ check_key,
+ letter_to_option,
ItemMenuResult,
- UniqueInventoryItem,
- BUC,
};
use crate::{
gamelog,
@@ -19,11 +19,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 +38,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 +95,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, _r, _n, 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 +118,7 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option (ItemMenuResult::NoResponse, None),
@@ -141,21 +126,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..7604527 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;
+use crate::invkeys::check_key;
+
mod character_creation;
mod cheat_menu;
mod letter_to_option;
@@ -101,6 +106,101 @@ pub fn draw_lerping_bar(
ctx.print(sx + width, sy, "]");
}
+fn draw_xp(ctx: &mut BTerm, pt: Point, pool: &Pools) {
+ ctx.print_color(
+ pt.x,
+ pt.y,
+ RGB::named(WHITE),
+ RGB::named(BLACK),
+ format!("XP{}/{}", pool.level, pool.xp)
+ );
+}
+
+fn calc_ac(ecs: &World, skills: &Skills, stats: &Pools, attr: &Attributes) -> i32 {
+ let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, skills);
+ let mut armour_ac_bonus = 0;
+ let equipped = ecs.read_storage::();
+ let ac = ecs.read_storage::();
+ let player_entity = ecs.fetch::();
+ for (wielded, ac) in (&equipped, &ac).join() {
+ if wielded.owner == *player_entity {
+ armour_ac_bonus += ac.amount;
+ }
+ }
+ stats.bac - attr.dexterity.bonus / 2 - skill_ac_bonus - armour_ac_bonus
+}
+
+fn draw_ac(ctx: &mut BTerm, pt: Point, ac: i32) {
+ ctx.print_color(pt.x, pt.y, RGB::named(PINK), RGB::named(BLACK), "AC");
+ ctx.print_color(pt.x + 2, pt.y, RGB::named(WHITE), RGB::named(BLACK), ac);
+}
+
+fn draw_attributes(ctx: &mut BTerm, pt: Point, a: &Attributes) {
+ ctx.print_color(pt.x, pt.y, RGB::named(RED), RGB::named(BLACK), "STR");
+ ctx.print_color(pt.x + 3, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.strength.base);
+ ctx.print_color(pt.x + 7, pt.y, RGB::named(GREEN), RGB::named(BLACK), "DEX");
+ ctx.print_color(pt.x + 10, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.dexterity.base);
+ ctx.print_color(pt.x + 14, pt.y, RGB::named(ORANGE), RGB::named(BLACK), "CON");
+ ctx.print_color(pt.x + 17, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.constitution.base);
+ ctx.print_color(pt.x, 54, RGB::named(CYAN), RGB::named(BLACK), "INT");
+ ctx.print_color(pt.x + 3, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.intelligence.base);
+ ctx.print_color(pt.x + 7, pt.y + 1, RGB::named(YELLOW), RGB::named(BLACK), "WIS");
+ ctx.print_color(pt.x + 10, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.wisdom.base);
+ ctx.print_color(pt.x + 14, pt.y + 1, RGB::named(PURPLE), RGB::named(BLACK), "CHA");
+ ctx.print_color(pt.x + 17, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.charisma.base);
+}
+
+fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) {
+ match hunger.state {
+ HungerState::Satiated => {
+ ctx.print_color_right(
+ pt.x,
+ pt.y,
+ get_hunger_colour(hunger.state),
+ RGB::named(BLACK),
+ "Satiated"
+ );
+ }
+ HungerState::Normal => {}
+ HungerState::Hungry => {
+ ctx.print_color_right(
+ pt.x,
+ pt.y,
+ get_hunger_colour(hunger.state),
+ RGB::named(BLACK),
+ "Hungry"
+ );
+ }
+ HungerState::Weak => {
+ ctx.print_color_right(
+ pt.x,
+ pt.y,
+ get_hunger_colour(hunger.state),
+ RGB::named(BLACK),
+ "Weak"
+ );
+ }
+ HungerState::Fainting => {
+ ctx.print_color_right(
+ pt.x,
+ pt.y,
+ get_hunger_colour(hunger.state),
+ RGB::named(BLACK),
+ "Fainting"
+ );
+ }
+ HungerState::Starving => {
+ ctx.print_color_right(
+ pt.x,
+ pt.y,
+ get_hunger_colour(hunger.state),
+ RGB::named(BLACK),
+ "Starving"
+ );
+ }
+ }
+}
+
pub fn draw_ui(ecs: &World, ctx: &mut BTerm) {
// Render stats
let pools = ecs.read_storage::();
@@ -137,111 +237,12 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) {
RGB::named(BLUE),
RGB::named(BLACK)
);
- // Draw AC
- let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*skills);
- let mut armour_ac_bonus = 0;
- let equipped = ecs.read_storage::();
- let ac = ecs.read_storage::();
- let player_entity = ecs.fetch::();
- for (wielded, ac) in (&equipped, &ac).join() {
- if wielded.owner == *player_entity {
- armour_ac_bonus += ac.amount;
- }
- }
- let armour_class =
- stats.bac - attributes.dexterity.bonus / 2 - skill_ac_bonus - armour_ac_bonus;
- ctx.print_color(26, 53, RGB::named(PINK), RGB::named(BLACK), "AC");
- ctx.print_color(28, 53, RGB::named(WHITE), RGB::named(BLACK), armour_class);
- // Draw level
- ctx.print_color(
- 26,
- 54,
- RGB::named(WHITE),
- RGB::named(BLACK),
- format!("XP{}/{}", stats.level, stats.xp)
- );
- // Draw attributes
- let x = 38;
- ctx.print_color(x, 53, RGB::named(RED), RGB::named(BLACK), "STR");
- ctx.print_color(x + 3, 53, RGB::named(WHITE), RGB::named(BLACK), attributes.strength.base);
- ctx.print_color(x + 7, 53, RGB::named(GREEN), RGB::named(BLACK), "DEX");
- ctx.print_color(
- x + 10,
- 53,
- RGB::named(WHITE),
- RGB::named(BLACK),
- attributes.dexterity.base
- );
- ctx.print_color(x + 14, 53, RGB::named(ORANGE), RGB::named(BLACK), "CON");
- ctx.print_color(
- x + 17,
- 53,
- RGB::named(WHITE),
- RGB::named(BLACK),
- attributes.constitution.base
- );
- ctx.print_color(x, 54, RGB::named(CYAN), RGB::named(BLACK), "INT");
- ctx.print_color(
- x + 3,
- 54,
- RGB::named(WHITE),
- RGB::named(BLACK),
- attributes.intelligence.base
- );
- ctx.print_color(x + 7, 54, RGB::named(YELLOW), RGB::named(BLACK), "WIS");
- ctx.print_color(x + 10, 54, RGB::named(WHITE), RGB::named(BLACK), attributes.wisdom.base);
- ctx.print_color(x + 14, 54, RGB::named(PURPLE), RGB::named(BLACK), "CHA");
- ctx.print_color(x + 17, 54, RGB::named(WHITE), RGB::named(BLACK), attributes.charisma.base);
- // Draw hunger
- match hunger.state {
- HungerState::Satiated => {
- ctx.print_color_right(
- 70,
- 53,
- get_hunger_colour(hunger.state),
- RGB::named(BLACK),
- "Satiated"
- );
- }
- HungerState::Normal => {}
- HungerState::Hungry => {
- ctx.print_color_right(
- 70,
- 53,
- get_hunger_colour(hunger.state),
- RGB::named(BLACK),
- "Hungry"
- );
- }
- HungerState::Weak => {
- ctx.print_color_right(
- 70,
- 53,
- get_hunger_colour(hunger.state),
- RGB::named(BLACK),
- "Weak"
- );
- }
- HungerState::Fainting => {
- ctx.print_color_right(
- 70,
- 53,
- get_hunger_colour(hunger.state),
- RGB::named(BLACK),
- "Fainting"
- );
- }
- HungerState::Starving => {
- ctx.print_color_right(
- 70,
- 53,
- get_hunger_colour(hunger.state),
- RGB::named(BLACK),
- "Starving"
- );
- }
- }
+ draw_ac(ctx, Point::new(26, 53), calc_ac(ecs, skills, stats, attributes));
+ draw_xp(ctx, Point::new(26, 54), stats);
+ draw_attributes(ctx, Point::new(38, 53), attributes);
+ draw_hunger(ctx, Point::new(70, 53), hunger);
// Burden
+ let player_entity = ecs.fetch::();
if let Some(burden) = burden.get(*player_entity) {
match burden.level {
crate::BurdenLevel::Burdened => {
@@ -271,41 +272,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 +296,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 +481,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 +532,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 +626,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 +821,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 +837,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 +919,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 +928,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 +954,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 +972,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 +980,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 +1006,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 +1019,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 +1031,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 +1441,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..68cea65 100644
--- a/src/gui/remove_curse_menu.rs
+++ b/src/gui/remove_curse_menu.rs
@@ -3,9 +3,11 @@ use super::{
item_colour_ecs,
obfuscate_name_ecs,
print_options,
- renderable_colour,
+ unique_ecs,
+ check_key,
+ letter_to_option,
ItemMenuResult,
- UniqueInventoryItem,
+ InventorySlot,
};
use crate::{
gamelog,
@@ -18,10 +20,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 +36,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,34 +90,19 @@ 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 (singular, plural) = obfuscate_name_ecs(&gs.ecs, entity);
- let beatitude_status = if
- let Some(beatitude) = gs.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: 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, _b, _r, _n, key) in build_cursed_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(InventorySlot {
+ item: entity,
+ count: 1,
+ idx: key.idx,
+ });
}
// Get display args
let width = get_max_inventory_width(&player_inventory);
@@ -128,7 +117,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 +125,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..70fb25c 100644
--- a/src/inventory/collection_system.rs
+++ b/src/inventory/collection_system.rs
@@ -12,6 +12,7 @@ use crate::{
ObfuscatedName,
Position,
WantsToPickupItem,
+ WantsToAssignKey,
};
use specs::prelude::*;
use crate::data::messages;
@@ -33,6 +34,7 @@ impl<'a> System<'a> for ItemCollectionSystem {
ReadStorage<'a, Beatitude>,
ReadExpect<'a, MasterDungeonMap>,
ReadStorage<'a, Charges>,
+ ReadStorage<'a, WantsToAssignKey>,
);
fn run(&mut self, data: Self::SystemData) {
@@ -48,17 +50,11 @@ impl<'a> System<'a> for ItemCollectionSystem {
beatitudes,
dm,
wands,
+ wants_key,
) = 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 +78,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..2cee2f4
--- /dev/null
+++ b/src/invkeys.rs
@@ -0,0 +1,59 @@
+use std::sync::Mutex;
+use std::collections::HashMap;
+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/map_builders/mod.rs b/src/map_builders/mod.rs
index b9dcbfe..7b38efe 100644
--- a/src/map_builders/mod.rs
+++ b/src/map_builders/mod.rs
@@ -1,8 +1,6 @@
use super::{ spawner, Map, Position, Rect, TileType };
use bracket_lib::prelude::*;
-mod room_accretion;
-use room_accretion::RoomAccretionBuilder;
mod bsp_dungeon;
use bsp_dungeon::BspDungeonBuilder;
mod bsp_interior;
@@ -447,7 +445,7 @@ pub fn level_builder(
initial_player_level: i32
) -> BuilderChain {
match id {
- ID_OVERMAP => room_accretion(),
+ ID_OVERMAP => overmap_builder(),
ID_TOWN => town_builder(id, rng, width, height, 0, initial_player_level),
ID_TOWN2 => forest_builder(id, rng, width, height, 1, initial_player_level),
ID_TOWN3 =>
@@ -492,10 +490,3 @@ pub fn level_builder(
fn diff(branch_id: i32, lvl_id: i32) -> i32 {
return lvl_id - branch_id;
}
-
-fn room_accretion() -> BuilderChain {
- let mut builder = BuilderChain::new(false, 110, 64, 64, 0, "room_accretion", "accretion", 0, 1);
- builder.start_with(RoomAccretionBuilder::new());
- builder.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE));
- builder
-}
diff --git a/src/map_builders/room_accretion/consts.rs b/src/map_builders/room_accretion/consts.rs
deleted file mode 100644
index 8c10d16..0000000
--- a/src/map_builders/room_accretion/consts.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-use lazy_static::lazy_static;
-use bracket_lib::prelude::*;
-
-pub const HEIGHT: usize = 64;
-pub const WIDTH: usize = 64;
-pub const HALLWAY_CHANCE: f32 = 0.5;
-pub const VERTICAL_CORRIDOR_MIN_LENGTH: i32 = 2;
-pub const VERTICAL_CORRIDOR_MAX_LENGTH: i32 = 9;
-pub const HORIZONTAL_CORRIDOR_MIN_LENGTH: i32 = 5;
-pub const HORIZONTAL_CORRIDOR_MAX_LENGTH: i32 = 15;
-
-pub enum Operator {
- LessThan,
- GreaterThan,
- LessThanEqualTo,
- GreaterThanEqualTo,
- EqualTo,
-}
-
-impl Operator {
- pub fn eval(&self, a: i32, b: i32) -> bool {
- match self {
- Operator::LessThan => a < b,
- Operator::GreaterThan => a > b,
- Operator::LessThanEqualTo => a <= b,
- Operator::GreaterThanEqualTo => a >= b,
- Operator::EqualTo => a == b,
- }
- }
- pub fn string(&self) -> &str {
- match self {
- Operator::LessThan => "<",
- Operator::GreaterThan => ">",
- Operator::LessThanEqualTo => "<=",
- Operator::GreaterThanEqualTo => ">=",
- Operator::EqualTo => "==",
- }
- }
-}
-
-pub struct CellRules {
- pub adjacent_type: i32,
- pub into: i32,
- pub operator: Operator,
- pub n: i32,
-}
-
-impl CellRules {
- const fn new(adjacent_type: i32, into: i32, operator: Operator, n: i32) -> CellRules {
- CellRules {
- adjacent_type,
- into,
- operator,
- n,
- }
- }
-}
-
-lazy_static! {
- pub static ref CA: Vec> = vec![
- vec![CellRules::new(1, 1, Operator::GreaterThanEqualTo, 4)],
- vec![
- CellRules::new(0, 0, Operator::GreaterThanEqualTo, 5),
- CellRules::new(1, 0, Operator::LessThan, 2)
- ]
- ];
-}
-
-#[derive(Debug, Copy, Clone, PartialEq)]
-pub enum Direction {
- NoDir = -1,
- North = 0,
- East = 1,
- South = 2,
- West = 3,
-}
-
-impl Direction {
- pub fn transform(&self) -> Point {
- match self {
- Direction::NoDir => unreachable!("Direction::NoDir should never be transformed"),
- Direction::North => Point::new(0, -1),
- Direction::East => Point::new(1, 0),
- Direction::South => Point::new(0, 1),
- Direction::West => Point::new(-1, 0),
- }
- }
- pub fn opposite_dir(&self) -> Direction {
- match self {
- Direction::NoDir => unreachable!("Direction::NoDir has no opposite."),
- Direction::North => Direction::South,
- Direction::East => Direction::West,
- Direction::South => Direction::North,
- Direction::West => Direction::East,
- }
- }
-}
-
-pub struct DirectionIterator {
- current: Direction,
-}
-
-impl DirectionIterator {
- pub fn new() -> DirectionIterator {
- DirectionIterator {
- current: Direction::North,
- }
- }
-}
-
-impl Iterator for DirectionIterator {
- type Item = Direction;
- fn next(&mut self) -> Option {
- use Direction::*;
- let next_direction = match self.current {
- North => East,
- East => South,
- South => West,
- West => {
- return None;
- }
- NoDir => unreachable!("Direction::NoDir should never be iterated over."),
- };
- self.current = next_direction;
- Some(next_direction)
- }
-}
diff --git a/src/map_builders/room_accretion/mod.rs b/src/map_builders/room_accretion/mod.rs
deleted file mode 100644
index 13f729b..0000000
--- a/src/map_builders/room_accretion/mod.rs
+++ /dev/null
@@ -1,397 +0,0 @@
-use super::{ BuilderMap, Map, InitialMapBuilder, TileType, Point };
-use bracket_lib::prelude::*;
-
-mod consts;
-use consts::*;
-
-/// Room Accretion map builder.
-pub struct RoomAccretionBuilder {}
-
-impl InitialMapBuilder for RoomAccretionBuilder {
- #[allow(dead_code)]
- fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
- self.build(rng, build_data);
- }
-}
-
-impl RoomAccretionBuilder {
- /// Constructor for Room Accretion.
- pub fn new() -> Box {
- Box::new(RoomAccretionBuilder {})
- }
-
- fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
- accrete_rooms(rng, build_data);
- }
-}
-
-fn grid_with_dimensions(h: usize, w: usize, value: i32) -> Vec> {
- vec![vec![value; w]; h]
-}
-
-fn in_bounds(row: i32, col: i32, build_data: &BuilderMap) -> bool {
- row > 0 && row < build_data.height && col > 0 && col < build_data.width
-}
-
-fn draw_continuous_shape_on_grid(
- room: &Vec>,
- top_offset: usize,
- left_offset: usize,
- grid: &mut Vec>
-) {
- for row in 0..room.len() {
- for col in 0..room[0].len() {
- if room[row][col] != 0 {
- let target_row = row + top_offset;
- let target_col = col + left_offset;
- if target_row < grid.len() && target_col < grid[0].len() {
- grid[target_row][target_col] = room[row][col];
- }
- }
- }
- }
-}
-
-struct Coordinate {
- pub location: Point,
- pub value: i32,
-}
-
-fn draw_individual_coordinates_on_grid(coordinates: &Vec, grid: &mut Vec>) {
- for c in coordinates {
- let x = c.location.x as usize;
- let y = c.location.y as usize;
- if y < grid.len() && x < grid[0].len() {
- grid[y][x] = c.value;
- }
- }
-}
-
-fn get_cell_neighbours(
- cells: &Vec>,
- row: usize,
- col: usize,
- h: usize,
- w: usize
-) -> Vec {
- let mut neighbours = Vec::new();
- for x in row.saturating_sub(1)..=std::cmp::min(row + 1, h - 1) {
- for y in col.saturating_sub(1)..=std::cmp::min(col + 1, w - 1) {
- if x != row || y != col {
- neighbours.push(cells[x][y]);
- }
- }
- }
- console::log(&format!("neighbours: {:?}", neighbours));
- neighbours
-}
-
-fn make_ca_room(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) -> Vec> {
- let width = rng.range(5, 12);
- let height = rng.range(5, 12);
- let mut cells = grid_with_dimensions(height, width, 0);
- cells = cells
- .into_iter()
- .map(|row| {
- row.into_iter()
- .map(|_| if rng.roll_dice(1, 2) == 1 { 1 } else { 0 })
- .collect()
- })
- .collect();
-
- let transform_cell = |state: i32, neighbours: &Vec| -> i32 {
- let rules: &[CellRules] = &CA[state as usize];
- let mut new_state = state;
- for rule in rules {
- let n_neighbours = neighbours
- .iter()
- .filter(|&&neighbour| neighbour == rule.adjacent_type)
- .count();
- if rule.operator.eval(n_neighbours as i32, rule.n) {
- new_state = rule.into;
- }
- }
- new_state
- };
-
- for _ in 0..5 {
- let mut new_cells = vec![vec![0; width]; height];
- for row in 0..height {
- for col in 0..width {
- let neighbours = get_cell_neighbours(&cells, row, col, height, width);
- let new_state = transform_cell(cells[row][col], &neighbours);
- new_cells[row][col] = new_state;
- }
- }
- cells = new_cells;
- }
- // TODO: Floodfill to keep largest contiguous blob
- cells
-}
-
-fn room_fits_at(
- hyperspace: Vec>,
- top_offset: usize,
- left_offset: usize,
- build_data: &BuilderMap
-) -> bool {
- let mut x_dungeon: usize;
- let mut y_dungeon: usize;
- for y in 0..HEIGHT {
- for x in 0..WIDTH {
- if hyperspace[y][x] != 2 {
- y_dungeon = y + top_offset;
- x_dungeon = x + left_offset;
- for i in y_dungeon.saturating_sub(1)..=std::cmp::min(y_dungeon + 1, WIDTH - 1) {
- for j in x_dungeon.saturating_sub(1)..=std::cmp::min(
- x_dungeon + 1,
- HEIGHT - 1
- ) {
- let pt = build_data.map.xy_idx(i as i32, j as i32);
- if
- !in_bounds(i as i32, j as i32, &build_data) ||
- !(build_data.map.tiles[pt] == TileType::Wall) ||
- build_data.spawn_list.contains(&(pt, "door".to_string()))
- {
- return false;
- }
- }
- }
- }
- }
- }
- return true;
-}
-
-fn direction_of_door(
- grid: &Vec>,
- row: usize,
- col: usize,
- build_data: &BuilderMap
-) -> Direction {
- if grid[row][col] != 0 {
- return Direction::NoDir;
- }
- let mut solution = Direction::NoDir;
- let mut dir_iter = DirectionIterator::new();
- for dir in &mut dir_iter {
- let new_col = (col as i32) + dir.transform().x;
- let new_row = (row as i32) + dir.transform().y;
- let opp_col = (col as i32) - dir.transform().x;
- let opp_row = (row as i32) - dir.transform().y;
- if
- in_bounds(new_row, new_col, &build_data) &&
- in_bounds(opp_row, opp_col, &build_data) &&
- grid[opp_row as usize][opp_col as usize] != 0
- {
- solution = dir;
- }
- }
- return solution;
-}
-
-#[derive(Debug, Copy, Clone, PartialEq)]
-pub struct DoorSite {
- pub x: i32,
- pub y: i32,
- pub dir: Direction,
-}
-
-fn choose_random_door_site(
- room: Vec>,
- rng: &mut RandomNumberGenerator,
- build_data: &BuilderMap
-) -> Vec