commit a63ec3d54ea04de6dbf9d990ec447beffa3be525
Author: Simon Watson <spw01@protonmail.com>
Date: Sun Oct 2 20:25:29 2022 -0400
Worked up to RangedScrolls/Targeting
diff --git a/notes.md b/notes.md
new file mode 100644
index 0000000..7168548
--- /dev/null
+++ b/notes.md
@@ -0,0 +1,19 @@
+# Saint Anthony's Fire
+
+I intend to branch off from the tutorial at some point and have the game be
+about playing as St. Anthony being tested in the desert. I think the core
+gameplay mechanics should be a spellcrafting system and a faith system.
+
+# Ideas
+* Spellcrafting System
+A limited number of discrete spell attributes that can be combined
+to form spells.
+
+* Faith System
+Some kind of 'mana' esque system that's based on player decision
+making. This may be beyond my abilities, but if thematically the player
+is playing as a Saint, there should be come component in the gameplay
+that relates to that.
+* * Sacrifice turns while in combat to gain faith (Pacifism)
+* * Convert HP to Faith (mana) (Flagilation) OR
+* * * No HP/Mana, shared resource pool that is used for HP and casting (Faith)
diff --git a/src/components.rs b/src/components.rs
index a124f16..812e3c2 100644
--- a/src/components.rs
+++ b/src/components.rs
@@ -3,6 +3,73 @@ use specs_derive::*;
use rltk::{RGB};
// COMPONENTS
+#[derive(Component, Debug)]
+pub struct Consumable {}
+
+#[derive(Component, Debug, Clone)]
+pub struct WantsToDropItem {
+ pub item : Entity
+}
+
+#[derive(Component, Debug)]
+pub struct WantsToDrinkPotion {
+ pub potion : Entity
+}
+
+#[derive(Component, Debug, Clone)]
+pub struct WantsToPickupItem {
+ pub collected_by : Entity,
+ pub item : Entity
+}
+
+#[derive(Component, Debug, Clone)]
+pub struct InBackpack {
+ pub owner : Entity
+}
+
+#[derive(Component, Debug)]
+pub struct Item {}
+
+#[derive(Component, Debug)]
+pub struct Potion {
+ pub heal_amount : i32
+}
+
+#[derive(Component, Debug)]
+pub struct SufferDamage {
+ pub amount : Vec<i32>
+}
+impl SufferDamage {
+ pub fn new_damage(store: &mut WriteStorage<SufferDamage>, victim: Entity, amount: i32) {
+ if let Some(suffering) = store.get_mut(victim) {
+ suffering.amount.push(amount);
+ } else {
+ let dmg = SufferDamage { amount : vec![amount] };
+ store.insert(victim, dmg).expect("Unable to insert damage");
+ }
+ }
+}
+
+#[derive(Component, Debug, Clone)]
+pub struct WantsToMelee {
+ pub target : Entity
+}
+
+#[derive(Component, Debug)]
+pub struct CombatStats {
+ pub max_hp : i32,
+ pub hp : i32,
+ pub defense : i32,
+ pub power : i32
+}
+
+#[derive(Component, Debug)]
+pub struct BlocksTile {}
+
+#[derive(Component, Debug)]
+pub struct Name {
+ pub name : String
+}
#[derive(Component, Debug)]
pub struct Monster {}
@@ -25,6 +92,7 @@ pub struct Renderable {
pub glyph: rltk::FontCharType,
pub fg: RGB,
pub bg: RGB,
+ pub render_order : i32
}
#[derive(Component, Debug)]
diff --git a/src/damage_system.rs b/src/damage_system.rs
new file mode 100644
index 0000000..b716d49
--- /dev/null
+++ b/src/damage_system.rs
@@ -0,0 +1,52 @@
+use specs::prelude::*;
+use super::{CombatStats, SufferDamage, Player, GameLog, Name};
+use rltk::{console};
+
+pub struct DamageSystem {}
+
+pub fn delete_the_dead(ecs : &mut World) {
+ let mut dead : Vec<Entity> = Vec::new();
+ // Using a scope to make the borrow checker happy
+ {
+ let combat_stats = ecs.read_storage::<CombatStats>();
+ let players = ecs.read_storage::<Player>();
+ let entities = ecs.entities();
+ let names = ecs.read_storage::<Name>();
+ let mut log = ecs.write_resource::<GameLog>();
+
+ for (entity, stats) in (&entities, &combat_stats).join() {
+ if stats.hp < 1 {
+ let player = players.get(entity);
+ match player {
+ None => {
+ let victim_name = names.get(entity);
+ if let Some(victim_name) = victim_name {
+ log.entries.push(format!("{} is dead", &victim_name.name));
+ }
+ dead.push(entity);
+ },
+ Some(_) => console::log("You are dead")
+ }
+ }
+ }
+ }
+
+ for victim in dead {
+ ecs.delete_entity(victim).expect("Unable to delete");
+ }
+}
+
+impl<'a> System<'a> for DamageSystem {
+ type SystemData = ( WriteStorage<'a, CombatStats>,
+ WriteStorage<'a, SufferDamage> );
+
+ fn run(&mut self, data : Self::SystemData) {
+ let (mut stats, mut damage) = data;
+
+ for (mut stats, damage) in (&mut stats, &damage).join() {
+ stats.hp -= damage.amount.iter().sum::<i32>();
+ }
+
+ damage.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/gamelog.rs b/src/gamelog.rs
new file mode 100644
index 0000000..e5f6977
--- /dev/null
+++ b/src/gamelog.rs
@@ -0,0 +1,4 @@
+
+pub struct GameLog {
+ pub entries : Vec<String>
+}
\ No newline at end of file
diff --git a/src/gui.rs b/src/gui.rs
new file mode 100644
index 0000000..a7e7634
--- /dev/null
+++ b/src/gui.rs
@@ -0,0 +1,179 @@
+use rltk::{ RGB, Rltk, VirtualKeyCode};
+use specs::prelude::*;
+use super::{CombatStats, Player, GameLog,
+ MAPHEIGHT, Map, Name,
+ Position, Point, InBackpack,
+ State};
+
+const GUI_HEIGHT: usize = 50 - MAPHEIGHT - 1;
+
+pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
+ ctx.draw_box(0, 38, 79, GUI_HEIGHT, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+
+ let combat_stats = ecs.read_storage::<CombatStats>();
+ let players = ecs.read_storage::<Player>();
+ for (_player, stats) in (&players, &combat_stats).join() {
+ let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
+ ctx.print_color(12, 38, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health);
+
+ ctx.draw_bar_horizontal(28, 38, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK));
+ }
+
+ let log = ecs.fetch::<GameLog>();
+
+ let mut y = 40;
+ for s in log.entries.iter().rev() {
+ if y < 49 { ctx.print(2, y, s); }
+ y += 1;
+ }
+
+ // Draw mouse cursor
+ let mouse_pos = ctx.mouse_pos();
+ ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::ANTIQUE_WHITE));
+
+ draw_tooltips(ecs, ctx);
+
+}
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum ItemMenuResult { Cancel, NoResponse, Selected }
+
+pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+ let player_entity = gs.ecs.fetch::<Entity>();
+ let names = gs.ecs.read_storage::<Name>();
+ let backpack = gs.ecs.read_storage::<InBackpack>();
+ let entities = gs.ecs.entities();
+
+ let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+ let count = inventory.count();
+
+ let mut y = (25 - (count / 2)) as i32;
+ ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+ ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Inventory");
+ ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+ let mut equippable : Vec<Entity> = Vec::new();
+ let mut j = 0;
+ for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+ ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+ ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as rltk::FontCharType);
+ ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+ ctx.print(21, y, &name.name.to_string());
+ equippable.push(entity);
+ y += 1;
+ j += 1;
+ }
+
+ match ctx.key {
+ None => (ItemMenuResult::NoResponse, None),
+ Some(key) => {
+ match key {
+ VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+ _ => {
+ let selection = rltk::letter_to_option(key);
+ if selection > -1 && selection < count as i32 {
+ return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+ }
+ (ItemMenuResult::NoResponse, None)
+ }
+ }
+ }
+ }
+}
+
+pub fn drop_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+ let player_entity = gs.ecs.fetch::<Entity>();
+ let names = gs.ecs.read_storage::<Name>();
+ let backpack = gs.ecs.read_storage::<InBackpack>();
+ let entities = gs.ecs.entities();
+
+ let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+ let count = inventory.count();
+
+ let mut y = (25 - (count / 2)) as i32;
+ ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+ ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Drop Which Item?");
+ ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+ let mut equippable : Vec<Entity> = Vec::new();
+ let mut j = 0;
+ for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+ ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+ ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as rltk::FontCharType);
+ ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+ ctx.print(21, y, &name.name.to_string());
+ equippable.push(entity);
+ y += 1;
+ j += 1;
+ }
+
+ match ctx.key {
+ None => (ItemMenuResult::NoResponse, None),
+ Some(key) => {
+ match key {
+ VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+ _ => {
+ let selection = rltk::letter_to_option(key);
+ if selection > -1 && selection < count as i32 {
+ return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+ }
+ (ItemMenuResult::NoResponse, None)
+ }
+ }
+ }
+ }
+}
+
+fn draw_tooltips(ecs: &World, ctx : &mut Rltk) {
+ let map = ecs.fetch::<Map>();
+ let names = ecs.read_storage::<Name>();
+ let positions = ecs.read_storage::<Position>();
+
+ let mouse_pos = ctx.mouse_pos();
+ if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { return; }
+ let mut tooltip : Vec<String> = Vec::new();
+ for (name, position) in (&names, &positions).join() {
+ let idx = map.xy_idx(position.x, position.y);
+ if position.x == mouse_pos.0 && position.y == mouse_pos.1 && map.visible_tiles[idx] {
+ tooltip.push(name.name.to_string());
+ }
+ }
+
+ if !tooltip.is_empty() {
+ let mut width :i32 = 0;
+ for s in tooltip.iter() {
+ if width < s.len() as i32 { width = s.len() as i32; }
+ }
+ width += 3;
+
+ if mouse_pos.0 > 40 {
+ let arrow_pos = Point::new(mouse_pos.0 - 2, mouse_pos.1);
+ let left_x = mouse_pos.0 - width;
+ let mut y = mouse_pos.1;
+ for s in tooltip.iter() {
+ ctx.print_color(left_x, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), s);
+ let padding = (width - s.len() as i32)-1;
+ for i in 0..padding {
+ ctx.print_color(arrow_pos.x - i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+ }
+ y += 1;
+ }
+ ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"->".to_string());
+ } else {
+ let arrow_pos = Point::new(mouse_pos.0 + 1, mouse_pos.1);
+ let left_x = mouse_pos.0 +3;
+ let mut y = mouse_pos.1;
+ for s in tooltip.iter() {
+ ctx.print_color(left_x + 1, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), s);
+ let padding = (width - s.len() as i32)-1;
+ for i in 0..padding {
+ ctx.print_color(arrow_pos.x + 1 + i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+ }
+ y += 1;
+ }
+ ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/inventory_system.rs b/src/inventory_system.rs
new file mode 100644
index 0000000..55a2330
--- /dev/null
+++ b/src/inventory_system.rs
@@ -0,0 +1,101 @@
+use specs::prelude::*;
+use super::{WantsToPickupItem, Name, InBackpack,
+ Position, GameLog, WantsToDrinkPotion,
+ CombatStats, Potion, WantsToDropItem};
+
+pub struct ItemCollectionSystem {}
+
+impl<'a> System<'a> for ItemCollectionSystem {
+ #[allow(clippy::type_complexity)]
+ type SystemData = ( ReadExpect<'a, Entity>,
+ WriteExpect<'a, GameLog>,
+ WriteStorage<'a, WantsToPickupItem>,
+ WriteStorage<'a, Position>,
+ ReadStorage<'a, Name>,
+ WriteStorage<'a, InBackpack>
+ );
+
+ fn run(&mut self, data : Self::SystemData) {
+ let (player_entity, mut gamelog, mut wants_pickup, mut positions, names, mut backpack) = data;
+
+ for pickup in wants_pickup.join() {
+ positions.remove(pickup.item);
+ backpack.insert(pickup.item, InBackpack{ owner: pickup.collected_by }).expect("Unable to insert backpack entry");
+
+ if pickup.collected_by == *player_entity {
+ gamelog.entries.push(format!("You pick up the {}.", names.get(pickup.item).unwrap().name));
+ }
+ }
+
+ wants_pickup.clear();
+ }
+}
+
+pub struct PotionUseSystem {}
+
+impl<'a> System<'a> for PotionUseSystem {
+ #[allow(clippy::type_complexity)]
+ type SystemData = ( ReadExpect<'a, Entity>,
+ WriteExpect<'a, GameLog>,
+ Entities<'a>,
+ WriteStorage<'a, WantsToDrinkPotion>,
+ ReadStorage<'a, Name>,
+ ReadStorage<'a, Potion>,
+ WriteStorage<'a, CombatStats>
+ );
+
+ fn run(&mut self, data : Self::SystemData) {
+ let (player_entity, mut gamelog, entities, mut wants_drink, names, potions, mut combat_stats) = data;
+
+ for (entity, drink, stats) in (&entities, &wants_drink, &mut combat_stats).join() {
+ let potion = potions.get(drink.potion);
+ match potion {
+ None => {}
+ Some(potion) => {
+ stats.hp = i32::min(stats.max_hp, stats.hp + potion.heal_amount);
+ if entity == *player_entity {
+ gamelog.entries.push(format!("You drink the {}, healing {} hp.", names.get(drink.potion).unwrap().name, potion.heal_amount));
+ }
+ entities.delete(drink.potion).expect("Delete failed");
+ }
+ }
+ }
+
+ wants_drink.clear();
+ }
+}
+
+pub struct ItemDropSystem {}
+
+impl<'a> System<'a> for ItemDropSystem {
+ #[allow(clippy::type_complexity)]
+ type SystemData = ( ReadExpect<'a, Entity>,
+ WriteExpect<'a, GameLog>,
+ Entities<'a>,
+ WriteStorage<'a, WantsToDropItem>,
+ ReadStorage<'a, Name>,
+ WriteStorage<'a, Position>,
+ WriteStorage<'a, InBackpack>
+ );
+
+ fn run(&mut self, data : Self::SystemData) {
+ let (player_entity, mut gamelog, entities, mut wants_drop, names, mut positions, mut backpack) = data;
+
+ for (entity, to_drop) in (&entities, &wants_drop).join() {
+ let mut dropper_pos : Position = Position{x:0, y:0};
+ {
+ let dropped_pos = positions.get(entity).unwrap();
+ dropper_pos.x = dropped_pos.x;
+ dropper_pos.y = dropped_pos.y;
+ }
+ positions.insert(to_drop.item, Position{ x : dropper_pos.x, y : dropper_pos.y }).expect("Unable to insert position");
+ backpack.remove(to_drop.item);
+
+ if entity == *player_entity {
+ gamelog.entries.push(format!("You drop the {}.", names.get(to_drop.item).unwrap().name));
+ }
+ }
+
+ wants_drop.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 7935308..64a740b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,4 @@
-use rltk::{GameState, Rltk, RGB};
+use rltk::{GameState, Rltk, Point};
use specs::prelude::*;
mod components;
@@ -13,19 +13,34 @@ mod visibility_system;
use visibility_system::VisibilitySystem;
mod monster_ai_system;
use monster_ai_system::MonsterAI;
+mod map_indexing_system;
+use map_indexing_system::MapIndexingSystem;
+mod melee_combat_system;
+use melee_combat_system::MeleeCombatSystem;
+mod damage_system;
+use damage_system::DamageSystem;
+mod gui;
+mod gamelog;
+use gamelog::GameLog;
+mod spawner;
+mod inventory_system;
+use inventory_system::*;
// ***** //
// STATE //
#[derive(PartialEq, Copy, Clone)]
pub enum RunState {
- Paused,
- Running
+ AwaitingInput,
+ PreRun,
+ PlayerTurn,
+ MonsterTurn,
+ ShowInventory,
+ ShowDropItem,
}
pub struct State {
pub ecs: World,
- pub runstate: RunState
}
impl State {
@@ -34,6 +49,18 @@ impl State {
vis.run_now(&self.ecs);
let mut mob = MonsterAI{};
mob.run_now(&self.ecs);
+ let mut mapindex = MapIndexingSystem{};
+ mapindex.run_now(&self.ecs);
+ let mut mcs = MeleeCombatSystem{};
+ mcs.run_now(&self.ecs);
+ let mut dmgs = DamageSystem{};
+ dmgs.run_now(&self.ecs);
+ let mut pickup = ItemCollectionSystem{};
+ pickup.run_now(&self.ecs);
+ let mut potions = PotionUseSystem{};
+ potions.run_now(&self.ecs);
+ let mut drop_items = ItemDropSystem{};
+ drop_items.run_now(&self.ecs);
self.ecs.maintain();
}
}
@@ -41,25 +68,79 @@ impl State {
impl GameState for State {
fn tick(&mut self, ctx : &mut Rltk) {
ctx.cls();
-
- if self.runstate == RunState::Running {
- self.run_systems();
- self.runstate = RunState::Paused;
- } else {
- self.runstate = player_input(self, ctx);
+ let mut newrunstate;
+ {
+ let runstate = self.ecs.fetch::<RunState>();
+ newrunstate = *runstate;
}
+ match newrunstate {
+ RunState::PreRun => {
+ self.run_systems();
+ self.ecs.maintain();
+ newrunstate = RunState::AwaitingInput;
+ }
+ RunState::AwaitingInput => {
+ newrunstate = player_input(self, ctx);
+ }
+ RunState::PlayerTurn => {
+ self.run_systems();
+ self.ecs.maintain();
+ newrunstate = RunState::MonsterTurn;
+ }
+ RunState::MonsterTurn => {
+ self.run_systems();
+ self.ecs.maintain();
+ newrunstate = RunState::AwaitingInput;
+ }
+ RunState::ShowInventory => {
+ let result = gui::show_inventory(self, ctx);
+ match result.0 {
+ gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+ gui::ItemMenuResult::NoResponse => {}
+ gui::ItemMenuResult::Selected => {
+ let item_entity = result.1.unwrap();
+ let mut intent = self.ecs.write_storage::<WantsToDrinkPotion>();
+ intent.insert(*self.ecs.fetch::<Entity>(), WantsToDrinkPotion{ potion: item_entity }).expect("Unable to insert intent");
+ newrunstate = RunState::PlayerTurn;
+ }
+ }
+ }
+ RunState::ShowDropItem => {
+ let result = gui::drop_item_menu(self, ctx);
+ match result.0 {
+ gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+ gui::ItemMenuResult::NoResponse => {}
+ gui::ItemMenuResult::Selected => {
+ let item_entity = result.1.unwrap();
+ let mut intent = self.ecs.write_storage::<WantsToDropItem>();
+ intent.insert(*self.ecs.fetch::<Entity>(), WantsToDropItem{ item: item_entity }).expect("Unable to insert intent");
+ newrunstate = RunState::PlayerTurn;
+ }
+ }
+ }
+ }
+
+ {
+ let mut runwriter = self.ecs.write_resource::<RunState>();
+ *runwriter = newrunstate;
+ }
+ damage_system::delete_the_dead(&mut self.ecs);
+
draw_map(&self.ecs, ctx);
let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>();
let map = self.ecs.fetch::<Map>();
- for (pos, render) in (&positions, &renderables).join() {
+ let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
+ data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
+ for (pos, render) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) }
}
-
+
+ gui::draw_ui(&self.ecs, ctx);
}
}
@@ -69,11 +150,13 @@ impl GameState for State {
fn main() -> rltk::BError {
use rltk::RltkBuilder;
let context = RltkBuilder::simple80x50()
- .with_title("Roguelike Tutorial")
+ .with_title("Saint Antony's Fire")
.build()?;
+ // Add scanlines
+ //context.with_post_scanlines(true);
+
let mut gs = State{
ecs: World::new(),
- runstate: RunState::Running
};
gs.ecs.register::<Position>();
@@ -81,47 +164,38 @@ fn main() -> rltk::BError {
gs.ecs.register::<Player>();
gs.ecs.register::<Viewshed>();
gs.ecs.register::<Monster>();
+ gs.ecs.register::<Name>();
+ gs.ecs.register::<BlocksTile>();
+ gs.ecs.register::<CombatStats>();
+ gs.ecs.register::<WantsToMelee>();
+ gs.ecs.register::<SufferDamage>();
+ gs.ecs.register::<Item>();
+ gs.ecs.register::<Potion>();
+ gs.ecs.register::<InBackpack>();
+ gs.ecs.register::<WantsToPickupItem>();
+ gs.ecs.register::<WantsToDrinkPotion>();
+ gs.ecs.register::<WantsToDropItem>();
+
let map = Map::new_map_rooms_and_corridors();
let (player_x, player_y) = map.rooms[0].center();
- let mut rng = rltk::RandomNumberGenerator::new();
- for room in map.rooms.iter().skip(1) {
- let (x,y) = room.center();
-
- let glyph : rltk::FontCharType;
- let roll = rng.roll_dice(1, 2);
- match roll {
- 1 => { glyph = rltk::to_cp437('g') }
- _ => { glyph = rltk::to_cp437('o') }
- }
+ let player_entity = spawner::player(&mut gs.ecs, player_x, player_y);
- gs.ecs.create_entity()
- .with(Position{ x, y })
- .with(Renderable{
- glyph: glyph,
- fg: RGB::named(rltk::RED),
- bg: RGB::named(rltk::BLACK),
- })
- .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
- .with(Monster{})
- .build();
+ gs.ecs.insert(player_entity);
+
+ let rng = rltk::RandomNumberGenerator::new();
+ gs.ecs.insert(rng);
+
+ for room in map.rooms.iter().skip(1) {
+ spawner::spawn_room(&mut gs.ecs, room);
}
gs.ecs.insert(map);
+ gs.ecs.insert(Point::new(player_x, player_y));
+ gs.ecs.insert(RunState::PreRun);
- gs.ecs
- .create_entity()
- .with(Position { x: player_x, y: player_y })
- .with(Renderable {
- glyph: rltk::to_cp437('@'),
- fg: RGB::named(rltk::YELLOW),
- bg: RGB::named(rltk::BLACK),
- })
- .with(Player{})
- .with(Viewshed{ visible_tiles : Vec::new(), range : 8, dirty: true })
- .build();
-
+ gs.ecs.insert(gamelog::GameLog{ entries : vec!["Saint Antony casts out...".to_string()] });
rltk::main_loop(context, gs)
}
\ No newline at end of file
diff --git a/src/map.rs b/src/map.rs
index d01dc64..917d102 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -3,6 +3,10 @@ use super::{Rect};
use std::cmp::{max, min};
use specs::prelude::*;
+pub const MAPWIDTH : usize = 80;
+pub const MAPHEIGHT : usize = 38;
+pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;
+
#[derive(PartialEq, Copy, Clone)]
pub enum TileType {
Wall,
@@ -15,9 +19,29 @@ pub struct Map {
pub width: i32,
pub height: i32,
pub revealed_tiles : Vec<bool>,
- pub visible_tiles: Vec<bool>
+ pub visible_tiles: Vec<bool>,
+ pub blocked : Vec<bool>,
+ pub tile_content : Vec<Vec<Entity>>
}
impl Map {
+ pub fn clear_content_index(&mut self) {
+ for content in self.tile_content.iter_mut() {
+ content.clear();
+ }
+ }
+
+ pub fn populate_blocked(&mut self) {
+ for (i,tile) in self.tiles.iter_mut().enumerate() {
+ self.blocked[i] = *tile == TileType::Wall;
+ }
+ }
+
+ fn is_exit_valid(&self, x:i32, y:i32) -> bool {
+ if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return false; }
+ let idx = self.xy_idx(x, y);
+ !self.blocked[idx]
+ }
+
pub fn xy_idx(&self, x: i32, y: i32) -> usize {
(y as usize * self.width as usize) + x as usize
}
@@ -51,12 +75,14 @@ impl Map {
pub fn new_map_rooms_and_corridors() -> Map {
let mut map = Map{
- tiles : vec![TileType::Wall; 80*50],
+ tiles : vec![TileType::Wall; MAPCOUNT],
rooms : Vec::new(),
- width : 80,
- height: 50,
- revealed_tiles : vec![false; 80*50],
- visible_tiles : vec![false; 80*50],
+ width : MAPWIDTH as i32,
+ height: MAPHEIGHT as i32,
+ revealed_tiles : vec![false; MAPCOUNT],
+ visible_tiles : vec![false; MAPCOUNT],
+ blocked : vec![false; MAPCOUNT],
+ tile_content : vec![Vec::new(); MAPCOUNT]
};
const MAX_ROOMS : i32 = 30;
@@ -109,12 +135,40 @@ impl BaseMap for Map {
fn is_opaque(&self, idx:usize) -> bool {
self.tiles[idx as usize] == TileType::Wall
}
+
+ fn get_available_exits(&self, idx:usize) -> rltk::SmallVec<[(usize, f32); 10]> {
+ let mut exits = rltk::SmallVec::new();
+ let x = idx as i32 % self.width;
+ let y = idx as i32 / self.width;
+ let w = self.width as usize;
+
+ // Cardinal directions
+ if self.is_exit_valid(x-1, y) { exits.push((idx-1, 1.0)) };
+ if self.is_exit_valid(x+1, y) { exits.push((idx+1, 1.0)) };
+ if self.is_exit_valid(x, y-1) { exits.push((idx-w, 1.0)) };
+ if self.is_exit_valid(x, y+1) { exits.push((idx+w, 1.0)) };
+
+ // Diagonals
+ if self.is_exit_valid(x-1, y-1) { exits.push(((idx-w)-1, 1.45)); }
+ if self.is_exit_valid(x+1, y-1) { exits.push(((idx-w)+1, 1.45)); }
+ if self.is_exit_valid(x-1, y+1) { exits.push(((idx+w)-1, 1.45)); }
+ if self.is_exit_valid(x+1, y+1) { exits.push(((idx+w)+1, 1.45)); }
+
+ exits
+ }
+
+ fn get_pathing_distance(&self, idx1:usize, idx2:usize) -> f32 {
+ let w = self.width as usize;
+ let p1 = Point::new(idx1 % w, idx1 / w);
+ let p2 = Point::new(idx2 % w, idx2 / w);
+ rltk::DistanceAlg::Pythagoras.distance2d(p1, p2)
+ }
}
// /// Makes a map with solid boundaries and 400 randomly placed walls. No guarantees that it won't
// /// look awful.
// pub fn new_map_test() -> Vec<TileType> {
-// let mut map = vec![TileType::Floor; 80*50];
+// let mut map = vec![TileType::Floor; MAPCOUNT];
// // Make the boundaries walls
// for x in 0..80 {
@@ -155,11 +209,11 @@ pub fn draw_map(ecs: &World, ctx : &mut Rltk) {
match tile {
TileType::Floor => {
glyph = rltk::to_cp437('.');
- fg = RGB::from_f32(0.0, 0.5, 0.5);
+ fg = RGB::from_f32(1.0, 0.5, 0.7);
}
TileType::Wall => {
glyph = rltk::to_cp437('#');
- fg = RGB::from_f32(0., 1.0, 0.);
+ fg = RGB::from_f32(1.0, 0.6, 0.);
}
}
if !map.visible_tiles[idx] { fg = fg.to_greyscale() }
diff --git a/src/map_indexing_system.rs b/src/map_indexing_system.rs
new file mode 100644
index 0000000..774c54e
--- /dev/null
+++ b/src/map_indexing_system.rs
@@ -0,0 +1,31 @@
+use specs::prelude::*;
+use super::{Map, Position, BlocksTile};
+
+pub struct MapIndexingSystem {}
+
+impl<'a> System<'a> for MapIndexingSystem {
+ type SystemData = ( WriteExpect<'a, Map>,
+ ReadStorage<'a, Position>,
+ ReadStorage<'a, BlocksTile>,
+ Entities<'a>,);
+
+ fn run(&mut self, data : Self::SystemData) {
+ let (mut map, position, blockers, entities) = data;
+
+ map.populate_blocked();
+ map.clear_content_index();
+ for (entity, position) in (&entities, &position).join() {
+ let idx = map.xy_idx(position.x, position.y);
+
+ // If they block, update the blocking list
+ let _p : Option<&BlocksTile> = blockers.get(entity);
+ if let Some(_p) = _p {
+ map.blocked[idx] = true;
+ }
+
+ // Push the entity to the appropriate index slot. It's a Copy
+ // type, so we don't need to clone it (we want to avoid moving it out of the ECS!)
+ map.tile_content[idx].push(entity);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs
new file mode 100644
index 0000000..10f0d72
--- /dev/null
+++ b/src/melee_combat_system.rs
@@ -0,0 +1,38 @@
+use specs::prelude::*;
+use super::{CombatStats, WantsToMelee, Name, SufferDamage, GameLog};
+
+pub struct MeleeCombatSystem {}
+
+impl<'a> System<'a> for MeleeCombatSystem {
+ type SystemData = ( Entities<'a>,
+ WriteExpect<'a, GameLog>,
+ WriteStorage<'a, WantsToMelee>,
+ ReadStorage<'a, Name>,
+ ReadStorage<'a, CombatStats>,
+ WriteStorage<'a, SufferDamage>
+ );
+
+ fn run(&mut self, data : Self::SystemData) {
+ let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data;
+
+ for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
+ if stats.hp > 0 {
+ let target_stats = combat_stats.get(wants_melee.target).unwrap();
+ if target_stats.hp > 0 {
+ let target_name = names.get(wants_melee.target).unwrap();
+
+ let damage = i32::max(0, stats.power - target_stats.defense);
+
+ if damage == 0 {
+ log.entries.push(format!("{} is unable to hurt {}", &name.name, &target_name.name));
+ } else {
+ log.entries.push(format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage));
+ SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage);
+ }
+ }
+ }
+ }
+
+ wants_melee.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/monster_ai_system.rs b/src/monster_ai_system.rs
index fc49af9..87af991 100644
--- a/src/monster_ai_system.rs
+++ b/src/monster_ai_system.rs
@@ -1,19 +1,46 @@
use specs::prelude::*;
-use super::{Viewshed, Position, Map, Monster};
-use rltk::{field_of_view, Point, console};
+use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState};
+use rltk::{Point};
pub struct MonsterAI {}
impl<'a> System<'a> for MonsterAI {
- type SystemData = ( ReadStorage<'a, Viewshed>,
- ReadStorage<'a, Position>,
- ReadStorage<'a, Monster>);
+ #[allow(clippy::type_complexity)]
+ type SystemData = ( WriteExpect<'a, Map>,
+ ReadExpect<'a, Point>,
+ ReadExpect<'a, Entity>,
+ ReadExpect<'a, RunState>,
+ Entities<'a>,
+ WriteStorage<'a, Viewshed>,
+ ReadStorage<'a, Monster>,
+ WriteStorage<'a, Position>,
+ WriteStorage<'a, WantsToMelee>);
fn run(&mut self, data : Self::SystemData) {
- let (viewshed, pos, monster) = data;
-
- for (viewshed,pos,_monster) in (&viewshed, &pos, &monster).join() {
- console::log("Monster considers their own existence");
+ let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee) = data;
+ if *runstate != RunState::MonsterTurn { return; }
+ for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() {
+ let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos);
+ if distance < 1.5 {
+ wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack");
+ }
+ else if viewshed.visible_tiles.contains(&*player_pos) {
+ // Path to the player
+ let path = rltk::a_star_search(
+ map.xy_idx(pos.x, pos.y),
+ map.xy_idx(player_pos.x, player_pos.y),
+ &mut *map
+ );
+ if path.success && path.steps.len()>1 {
+ let mut idx = map.xy_idx(pos.x, pos.y);
+ map.blocked[idx] = false;
+ pos.x = path.steps[1] as i32 % map.width;
+ pos.y = path.steps[1] as i32 / map.width;
+ idx = map.xy_idx(pos.x, pos.y);
+ map.blocked[idx] = true;
+ viewshed.dirty = true;
+ }
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/src/player.rs b/src/player.rs
index a27b082..493acd6 100644
--- a/src/player.rs
+++ b/src/player.rs
@@ -1,20 +1,64 @@
-use rltk::{VirtualKeyCode, Rltk};
+use rltk::{VirtualKeyCode, Rltk, Point};
use specs::prelude::*;
-use super::{Position, Player, TileType, Map, State, Viewshed, RunState};
+use super::{Position, Player, WantsToMelee,
+ Map, State, Viewshed,
+ RunState, CombatStats, WantsToPickupItem,
+ GameLog, Item};
use std::cmp::{min, max};
+fn get_item(ecs: &mut World) {
+ let player_pos = ecs.fetch::<Point>();
+ let player_entity = ecs.fetch::<Entity>();
+ let entities = ecs.entities();
+ let items = ecs.read_storage::<Item>();
+ let positions = ecs.read_storage::<Position>();
+ let mut gamelog = ecs.fetch_mut::<GameLog>();
+
+ let mut target_item : Option<Entity> = None;
+ for (item_entity, _item, position) in (&entities, &items, &positions).join() {
+ if position.x == player_pos.x && position.y == player_pos.y {
+ target_item = Some(item_entity);
+ }
+ }
+
+ match target_item {
+ None => gamelog.entries.push("There is nothing here to pick up.".to_string()),
+ Some(item) => {
+ let mut pickup = ecs.write_storage::<WantsToPickupItem>();
+ pickup.insert(*player_entity, WantsToPickupItem{ collected_by: *player_entity, item }).expect("Unable to insert want to pickup");
+ }
+ }
+}
+
pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
let mut positions = ecs.write_storage::<Position>();
- let mut players = ecs.write_storage::<Player>();
+ let players = ecs.write_storage::<Player>();
let mut viewsheds = ecs.write_storage::<Viewshed>();
+ let combat_stats = ecs.read_storage::<CombatStats>();
let map = ecs.fetch::<Map>();
+ let entities = ecs.entities();
+ let mut wants_to_melee = ecs.write_storage::<WantsToMelee>();
- for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() {
+ for (entity, _player, pos, viewshed) in (&entities, &players, &mut positions, &mut viewsheds).join() {
+ if pos.x + delta_x < 1 || pos.x + delta_x > map.width-1 || pos.y + delta_y < 1 || pos.y + delta_y > map.height-1 { return; }
let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);
- if map.tiles[destination_idx] != TileType::Wall {
+
+ for potential_target in map.tile_content[destination_idx].iter() {
+ let target = combat_stats.get(*potential_target);
+ if let Some(_target) = target {
+ wants_to_melee.insert(entity, WantsToMelee{ target: *potential_target }).expect("Add target failed");
+ return;
+ }
+ }
+
+ if !map.blocked[destination_idx] {
pos.x = min(79 , max(0, pos.x + delta_x));
pos.y = min(49, max(0, pos.y + delta_y));
+ let mut ppos = ecs.write_resource::<Point>();
+ ppos.x = pos.x;
+ ppos.y = pos.y;
+
viewshed.dirty = true;
}
}
@@ -23,7 +67,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
// Player movement
match ctx.key {
- None => {} // Nothing happened
+ None => { return RunState::AwaitingInput } // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left |
VirtualKeyCode::Numpad4 |
@@ -45,8 +89,29 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
VirtualKeyCode::S |
VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
- _ => {}
+ // Diagonals
+ VirtualKeyCode::Numpad9 |
+ VirtualKeyCode::E |
+ VirtualKeyCode::Y => try_move_player(1, -1, &mut gs.ecs),
+
+ VirtualKeyCode::Numpad7 |
+ VirtualKeyCode::Q |
+ VirtualKeyCode::U => try_move_player(-1, -1, &mut gs.ecs),
+
+ VirtualKeyCode::Numpad3 |
+ VirtualKeyCode::C |
+ VirtualKeyCode::N => try_move_player(1, 1, &mut gs.ecs),
+
+ VirtualKeyCode::Numpad1 |
+ VirtualKeyCode::Z |
+ VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs),
+
+ VirtualKeyCode::G => get_item(&mut gs.ecs),
+ VirtualKeyCode::I => return RunState::ShowInventory,
+ VirtualKeyCode::X => return RunState::ShowDropItem,
+
+ _ => { return RunState::AwaitingInput }
},
}
- RunState::Running
+ RunState::PlayerTurn
}
\ No newline at end of file
diff --git a/src/spawner.rs b/src/spawner.rs
new file mode 100644
index 0000000..2fe0807
--- /dev/null
+++ b/src/spawner.rs
@@ -0,0 +1,129 @@
+use rltk::{ RGB, RandomNumberGenerator };
+use specs::prelude::*;
+use super::{CombatStats, Player, Renderable,
+ Name, Position, Viewshed,
+ Monster, BlocksTile, Rect,
+ MAPWIDTH, Item, Potion};
+
+const MAX_MONSTERS : i32 = 4;
+const MAX_ITEMS : i32 = 2;
+
+fn health_potion(ecs: &mut World, x: i32, y: i32) {
+ ecs.create_entity()
+ .with(Position{ x, y })
+ .with(Renderable{
+ glyph: rltk::to_cp437('ยก'),
+ fg: RGB::named(rltk::MAGENTA),
+ bg: RGB::named(rltk::BLACK),
+ render_order: 2
+ })
+ .with(Name{ name : "Health Potion".to_string() })
+ .with(Item{})
+ .with(Potion{ heal_amount: 12 })
+ .build();
+}
+
+/// Fills a room with stuff!
+pub fn spawn_room(ecs: &mut World, room : &Rect) {
+ let mut monster_spawn_points : Vec<usize> = Vec::new();
+ let mut item_spawn_points : Vec<usize> = Vec::new();
+
+ // Scope to keep the borrow checker happy
+ {
+ let mut rng = ecs.write_resource::<RandomNumberGenerator>();
+ let num_monsters = rng.roll_dice(1, MAX_MONSTERS + 2) - 3;
+ let num_items = rng.roll_dice(1, MAX_ITEMS + 2) - 3;
+
+
+ for _i in 0 .. num_monsters {
+ let mut added = false;
+ while !added {
+ let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
+ let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
+ let idx = (y * MAPWIDTH) + x;
+ if !monster_spawn_points.contains(&idx) {
+ monster_spawn_points.push(idx);
+ added = true;
+ }
+ }
+ }
+
+ for _i in 0 .. num_items {
+ let mut added = false;
+ while !added {
+ let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
+ let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
+ let idx = (y * MAPWIDTH) + x;
+ if !item_spawn_points.contains(&idx) {
+ item_spawn_points.push(idx);
+ added = true;
+ }
+ }
+ }
+ }
+
+ // Actually spawn the monsters
+ for idx in monster_spawn_points.iter() {
+ let x = *idx % MAPWIDTH;
+ let y = *idx / MAPWIDTH;
+ random_monster(ecs, x as i32, y as i32);
+ }
+
+ // Actually spawn the potions
+ for idx in item_spawn_points.iter() {
+ let x = *idx % MAPWIDTH;
+ let y = *idx / MAPWIDTH;
+ health_potion(ecs, x as i32, y as i32);
+ }
+}
+
+/// Spawns the player and returns his/her entity object.
+pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
+ ecs
+ .create_entity()
+ .with(Position { x: player_x, y: player_y })
+ .with(Renderable {
+ glyph: rltk::to_cp437('@'),
+ fg: RGB::named(rltk::YELLOW),
+ bg: RGB::named(rltk::BLACK),
+ render_order: 0
+ })
+ .with(Player{})
+ .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+ .with(Name{name: "Player".to_string() })
+ .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
+ .build()
+}
+
+/// Spawns a random monster at a given location
+pub fn random_monster(ecs: &mut World, x: i32, y: i32) {
+ let roll :i32;
+ {
+ let mut rng = ecs.write_resource::<RandomNumberGenerator>();
+ roll = rng.roll_dice(1, 2);
+ }
+ match roll {
+ 1 => { orc(ecs, x, y) }
+ _ => { goblin(ecs, x, y) }
+ }
+}
+
+fn orc(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('o'), "Orc"); }
+fn goblin(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('g'), "Goblin"); }
+
+fn monster<S : ToString>(ecs: &mut World, x: i32, y: i32, glyph : rltk::FontCharType, name : S) {
+ ecs.create_entity()
+ .with(Position{ x, y })
+ .with(Renderable{
+ glyph,
+ fg: RGB::named(rltk::RED),
+ bg: RGB::named(rltk::BLACK),
+ render_order: 1
+ })
+ .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+ .with(Monster{})
+ .with(Name{ name : name.to_string() })
+ .with(BlocksTile{})
+ .with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })
+ .build();
+}
\ No newline at end of file