magic missile, fireball scrolls

This commit is contained in:
Llywelwyn 2023-07-09 09:12:21 +01:00
parent 2266998e80
commit 06c3d40c65
7 changed files with 315 additions and 50 deletions

View file

@ -71,7 +71,22 @@ pub struct Item {}
#[derive(Component, Debug)] #[derive(Component, Debug)]
pub struct ProvidesHealing { pub struct ProvidesHealing {
pub heal_amount: i32, pub amount: i32,
}
#[derive(Component, Debug)]
pub struct InflictsDamage {
pub amount: i32,
}
#[derive(Component, Debug)]
pub struct Ranged {
pub range: i32,
}
#[derive(Component, Debug)]
pub struct AOE {
pub radius: i32,
} }
#[derive(Component, Debug, Clone)] #[derive(Component, Debug, Clone)]
@ -93,6 +108,7 @@ pub struct WantsToDropItem {
#[derive(Component, Debug)] #[derive(Component, Debug)]
pub struct WantsToUseItem { pub struct WantsToUseItem {
pub item: Entity, pub item: Entity,
pub target: Option<rltk::Point>,
} }
#[derive(Component, Debug)] #[derive(Component, Debug)]

View file

@ -1,4 +1,4 @@
use super::{gamelog::GameLog, CombatStats, Entities, Map, Name, Player, Position, SufferDamage}; use super::{gamelog::GameLog, CombatStats, Entities, Item, Map, Name, Player, Position, SufferDamage};
use specs::prelude::*; use specs::prelude::*;
pub struct DamageSystem {} pub struct DamageSystem {}
@ -35,6 +35,7 @@ pub fn delete_the_dead(ecs: &mut World) {
let combat_stats = ecs.read_storage::<CombatStats>(); let combat_stats = ecs.read_storage::<CombatStats>();
let players = ecs.read_storage::<Player>(); let players = ecs.read_storage::<Player>();
let names = ecs.read_storage::<Name>(); let names = ecs.read_storage::<Name>();
let items = ecs.read_storage::<Item>();
let entities = ecs.entities(); let entities = ecs.entities();
let mut log = ecs.write_resource::<GameLog>(); let mut log = ecs.write_resource::<GameLog>();
for (entity, stats) in (&entities, &combat_stats).join() { for (entity, stats) in (&entities, &combat_stats).join() {
@ -44,7 +45,12 @@ pub fn delete_the_dead(ecs: &mut World) {
None => { None => {
let victim_name = names.get(entity); let victim_name = names.get(entity);
if let Some(victim_name) = victim_name { if let Some(victim_name) = victim_name {
log.entries.push(format!("{} died!", &victim_name.name)); let item = items.get(entity);
if let Some(_item) = item {
log.entries.push(format!("{} was destroyed!", &victim_name.name));
} else {
log.entries.push(format!("{} died!", &victim_name.name));
}
} }
dead.push(entity) dead.push(entity)
} }

View file

@ -1,6 +1,6 @@
use super::{ use super::{
gamelog::GameLog, rex_assets::RexAssets, CombatStats, InBackpack, Map, Name, Player, Point, Position, RunState, gamelog::GameLog, rex_assets::RexAssets, CombatStats, InBackpack, Map, Name, Player, Point, Position, RunState,
State, State, Viewshed,
}; };
use rltk::{Rltk, VirtualKeyCode, RGB}; use rltk::{Rltk, VirtualKeyCode, RGB};
use specs::prelude::*; use specs::prelude::*;
@ -205,6 +205,60 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option
} }
} }
pub fn ranged_target(gs: &mut State, ctx: &mut Rltk, range: i32, aoe: i32) -> (ItemMenuResult, Option<Point>) {
let player_entity = gs.ecs.fetch::<Entity>();
let player_pos = gs.ecs.fetch::<Point>();
let viewsheds = gs.ecs.read_storage::<Viewshed>();
ctx.print_color(5, 0, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "select target");
// Highlight available cells
let mut available_cells = Vec::new();
let visible = viewsheds.get(*player_entity);
if let Some(visible) = visible {
// We have a viewshed
for idx in visible.visible_tiles.iter() {
let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx);
if distance <= range as f32 {
ctx.set_bg(idx.x, idx.y, RGB::named(rltk::BLUE));
available_cells.push(idx);
}
}
} else {
return (ItemMenuResult::Cancel, None);
}
// Draw mouse cursor
let mouse_pos = ctx.mouse_pos();
let map = gs.ecs.fetch::<Map>();
let mut valid_target = false;
for idx in available_cells.iter() {
if idx.x == mouse_pos.0 && idx.y == mouse_pos.1 {
valid_target = true;
}
}
if valid_target {
if aoe > 0 {
let mut blast_tiles = rltk::field_of_view(Point::new(mouse_pos.0, mouse_pos.1), aoe, &*map);
blast_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1);
for tile in blast_tiles.iter() {
ctx.set_bg(tile.x, tile.y, RGB::named(rltk::DARKCYAN));
}
}
ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN));
if ctx.left_click {
return (ItemMenuResult::Selected, Some(Point::new(mouse_pos.0, mouse_pos.1)));
}
} else {
ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED));
if ctx.left_click {
return (ItemMenuResult::Cancel, None);
}
}
(ItemMenuResult::NoResponse, None)
}
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
pub enum MainMenuSelection { pub enum MainMenuSelection {
NewGame, NewGame,

View file

@ -1,6 +1,6 @@
use super::{ use super::{
gamelog::GameLog, CombatStats, Consumable, InBackpack, Name, ParticleBuilder, Position, ProvidesHealing, gamelog::GameLog, CombatStats, Consumable, InBackpack, InflictsDamage, Map, Name, ParticleBuilder, Position,
WantsToDropItem, WantsToPickupItem, WantsToUseItem, DEFAULT_PARTICLE_LIFETIME, ProvidesHealing, SufferDamage, WantsToDropItem, WantsToPickupItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME,
}; };
use specs::prelude::*; use specs::prelude::*;
@ -39,65 +39,156 @@ impl<'a> System<'a> for ItemUseSystem {
type SystemData = ( type SystemData = (
ReadExpect<'a, Entity>, ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>, WriteExpect<'a, GameLog>,
ReadExpect<'a, Map>,
Entities<'a>, Entities<'a>,
WriteStorage<'a, WantsToUseItem>, WriteStorage<'a, WantsToUseItem>,
ReadStorage<'a, Name>, ReadStorage<'a, Name>,
ReadStorage<'a, Consumable>, ReadStorage<'a, Consumable>,
ReadStorage<'a, ProvidesHealing>, ReadStorage<'a, ProvidesHealing>,
WriteStorage<'a, CombatStats>, WriteStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, ParticleBuilder>, WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>, ReadStorage<'a, Position>,
ReadStorage<'a, InflictsDamage>,
ReadStorage<'a, AOE>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let ( let (
player_entity, player_entity,
mut gamelog, mut gamelog,
map,
entities, entities,
mut wants_use, mut wants_to_use,
names, names,
consumables, consumables,
healing, provides_healing,
mut combat_stats, mut combat_stats,
mut suffer_damage,
mut particle_builder, mut particle_builder,
positions, positions,
inflicts_damage,
aoe,
) = data; ) = data;
for (entity, use_item, stats) in (&entities, &wants_use, &mut combat_stats).join() { for (entity, wants_to_use) in (&entities, &wants_to_use).join() {
let item_heals = healing.get(use_item.item); let mut used_item = true;
match item_heals { let mut aoe_item = false;
None => {} let item_being_used = names.get(wants_to_use.item).unwrap();
Some(healer) => {
stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount); // TARGETING
let pos = positions.get(entity); let mut targets: Vec<Entity> = Vec::new();
if let Some(pos) = pos { match wants_to_use.target {
particle_builder.request( None => {
pos.x, targets.push(*player_entity);
pos.y, }
rltk::RGB::named(rltk::GREEN), Some(target) => {
rltk::RGB::named(rltk::BLACK), let area_effect = aoe.get(wants_to_use.item);
rltk::to_cp437('♥'), match area_effect {
DEFAULT_PARTICLE_LIFETIME, None => {
); // Single target in a tile
} let idx = map.xy_idx(target.x, target.y);
if entity == *player_entity { for mob in map.tile_content[idx].iter() {
gamelog.entries.push(format!( targets.push(*mob);
"You quaff the {}, and heal {} hp.", }
names.get(use_item.item).unwrap().name, }
healer.heal_amount Some(area_effect) => {
)); // AOE
} aoe_item = true;
let consumable = consumables.get(use_item.item); let mut blast_tiles = rltk::field_of_view(target, area_effect.radius, &*map);
match consumable { blast_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1);
None => {} for tile_idx in blast_tiles.iter() {
Some(_) => { let idx = map.xy_idx(tile_idx.x, tile_idx.y);
entities.delete(use_item.item).expect("Delete failed"); for mob in map.tile_content[idx].iter() {
targets.push(*mob);
}
particle_builder.request(
tile_idx.x,
tile_idx.y,
rltk::RGB::named(rltk::ORANGE),
rltk::RGB::named(rltk::BLACK),
rltk::to_cp437('░'),
200.0,
);
}
} }
} }
} }
} }
// HEALING ITEM
let item_heals = provides_healing.get(wants_to_use.item);
match item_heals {
None => {}
Some(heal) => {
for target in targets.iter() {
let stats = combat_stats.get_mut(*target);
if let Some(stats) = stats {
stats.hp = i32::min(stats.max_hp, stats.hp + heal.amount);
if entity == *player_entity {
gamelog.entries.push(format!(
"You quaff the {}, and heal {} hp.",
item_being_used.name, heal.amount
));
}
let pos = positions.get(entity);
if let Some(pos) = pos {
particle_builder.request(
pos.x,
pos.y,
rltk::RGB::named(rltk::GREEN),
rltk::RGB::named(rltk::BLACK),
rltk::to_cp437('♥'),
DEFAULT_PARTICLE_LIFETIME,
);
}
}
}
}
}
// DAMAGING ITEM
let item_damages = inflicts_damage.get(wants_to_use.item);
match item_damages {
None => {}
Some(damage) => {
let target_point = wants_to_use.target.unwrap();
gamelog.entries.push(format!("You use the {}!", item_being_used.name));
if !aoe_item {
particle_builder.request_star(
target_point.x,
target_point.y,
rltk::RGB::named(rltk::CYAN),
rltk::RGB::named(rltk::BLACK),
rltk::to_cp437('*'),
DEFAULT_PARTICLE_LIFETIME,
);
}
for mob in targets.iter() {
SufferDamage::new_damage(&mut suffer_damage, *mob, damage.amount);
if entity == *player_entity {
let mob_name = names.get(*mob).unwrap();
gamelog.entries.push(format!(
"{} takes {} damage from the {}!",
mob_name.name, damage.amount, item_being_used.name
));
}
used_item = true;
}
}
}
if used_item {
let consumable = consumables.get(wants_to_use.item);
match consumable {
None => {}
Some(_) => {
entities.delete(wants_to_use.item).expect("Delete failed");
}
}
}
} }
wants_use.clear(); wants_to_use.clear();
} }
} }

View file

@ -42,6 +42,7 @@ pub enum RunState {
MonsterTurn, MonsterTurn,
ShowInventory, ShowInventory,
ShowDropItem, ShowDropItem,
ShowTargeting { range: i32, item: Entity, aoe: i32 },
MainMenu { menu_selection: gui::MainMenuSelection }, MainMenu { menu_selection: gui::MainMenuSelection },
} }
@ -139,11 +140,28 @@ impl GameState for State {
gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => { gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap(); let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::<WantsToUseItem>(); let is_ranged = self.ecs.read_storage::<Ranged>();
intent let ranged_item = is_ranged.get(item_entity);
.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem { item: item_entity }) if let Some(ranged_item) = ranged_item {
.expect("Unable to insert intent."); let is_aoe = self.ecs.read_storage::<AOE>();
new_runstate = RunState::PlayerTurn; let aoe_item = is_aoe.get(item_entity);
if let Some(aoe_item) = aoe_item {
new_runstate = RunState::ShowTargeting {
range: ranged_item.range,
item: item_entity,
aoe: aoe_item.radius,
}
} else {
new_runstate =
RunState::ShowTargeting { range: ranged_item.range, item: item_entity, aoe: 0 }
}
} else {
let mut intent = self.ecs.write_storage::<WantsToUseItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem { item: item_entity, target: None })
.expect("Unable to insert intent.");
new_runstate = RunState::PlayerTurn;
}
} }
} }
} }
@ -162,6 +180,20 @@ impl GameState for State {
} }
} }
} }
RunState::ShowTargeting { range, item, aoe } => {
let result = gui::ranged_target(self, ctx, range, aoe);
match result.0 {
gui::ItemMenuResult::Cancel => new_runstate = RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let mut intent = self.ecs.write_storage::<WantsToUseItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem { item, target: result.1 })
.expect("Unable to insert intent.");
new_runstate = RunState::PlayerTurn;
}
}
}
RunState::MainMenu { .. } => { RunState::MainMenu { .. } => {
let result = gui::main_menu(self, ctx); let result = gui::main_menu(self, ctx);
match result { match result {
@ -225,6 +257,9 @@ fn main() -> rltk::BError {
gs.ecs.register::<SufferDamage>(); gs.ecs.register::<SufferDamage>();
gs.ecs.register::<Item>(); gs.ecs.register::<Item>();
gs.ecs.register::<ProvidesHealing>(); gs.ecs.register::<ProvidesHealing>();
gs.ecs.register::<InflictsDamage>();
gs.ecs.register::<Ranged>();
gs.ecs.register::<AOE>();
gs.ecs.register::<InBackpack>(); gs.ecs.register::<InBackpack>();
gs.ecs.register::<WantsToPickupItem>(); gs.ecs.register::<WantsToPickupItem>();
gs.ecs.register::<WantsToDropItem>(); gs.ecs.register::<WantsToDropItem>();
@ -245,7 +280,9 @@ fn main() -> rltk::BError {
gs.ecs.insert(map); gs.ecs.insert(map);
gs.ecs.insert(Point::new(player_x, player_y)); gs.ecs.insert(Point::new(player_x, player_y));
gs.ecs.insert(player_entity); gs.ecs.insert(player_entity);
gs.ecs.insert(gamelog::GameLog { entries: vec!["Here's your welcome message.".to_string()] }); gs.ecs.insert(gamelog::GameLog {
entries: vec!["<pretend i wrote a paragraph explaining why you're here>".to_string()],
});
gs.ecs.insert(RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame }); gs.ecs.insert(RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame });
gs.ecs.insert(particle_system::ParticleBuilder::new()); gs.ecs.insert(particle_system::ParticleBuilder::new());
gs.ecs.insert(rex_assets::RexAssets::new()); gs.ecs.insert(rex_assets::RexAssets::new());

View file

@ -4,6 +4,11 @@ use specs::prelude::*;
pub const DEFAULT_PARTICLE_LIFETIME: f32 = 150.0; pub const DEFAULT_PARTICLE_LIFETIME: f32 = 150.0;
/// Runs each tick, deleting particles who are past their expiry.
// Should make an addition to this to also spawn delayed particles,
// running through a list and removing the frame_time_ms from the
// delay. When delay is <= 0, make a particle_builder.request for
// the particle.
pub fn cull_dead_particles(ecs: &mut World, ctx: &Rltk) { pub fn cull_dead_particles(ecs: &mut World, ctx: &Rltk) {
let mut dead_particles: Vec<Entity> = Vec::new(); let mut dead_particles: Vec<Entity> = Vec::new();
{ {
@ -41,9 +46,28 @@ impl ParticleBuilder {
ParticleBuilder { requests: Vec::new() } ParticleBuilder { requests: Vec::new() }
} }
/// Makes a single particle request.
pub fn request(&mut self, x: i32, y: i32, fg: RGB, bg: RGB, glyph: rltk::FontCharType, lifetime: f32) { pub fn request(&mut self, x: i32, y: i32, fg: RGB, bg: RGB, glyph: rltk::FontCharType, lifetime: f32) {
self.requests.push(ParticleRequest { x, y, fg, bg, glyph, lifetime }); self.requests.push(ParticleRequest { x, y, fg, bg, glyph, lifetime });
} }
// Makes a particle request in the shape of an 'x'. Sort of.
pub fn request_star(&mut self, x: i32, y: i32, fg: RGB, bg: RGB, glyph: rltk::FontCharType, lifetime: f32) {
self.request(x, y, fg, bg, glyph, lifetime * 2.0);
self.request(x + 1, y + 1, fg, bg, rltk::to_cp437('/'), lifetime);
self.request(x + 1, y - 1, fg, bg, rltk::to_cp437('\\'), lifetime);
self.request(x - 1, y + 1, fg, bg, rltk::to_cp437('\\'), lifetime);
self.request(x - 1, y - 1, fg, bg, rltk::to_cp437('/'), lifetime);
}
/// Makes a particle request in the shape of a +.
pub fn request_plus(&mut self, x: i32, y: i32, fg: RGB, bg: RGB, glyph: rltk::FontCharType, lifetime: f32) {
self.request(x, y, fg, bg, glyph, lifetime * 2.0);
self.request(x + 1, y, fg, bg, rltk::to_cp437('─'), lifetime);
self.request(x - 1, y, fg, bg, rltk::to_cp437('─'), lifetime);
self.request(x, y + 1, fg, bg, rltk::to_cp437('│'), lifetime);
self.request(x, y - 1, fg, bg, rltk::to_cp437('│'), lifetime);
}
} }
pub struct ParticleSpawnSystem {} pub struct ParticleSpawnSystem {}

View file

@ -1,6 +1,6 @@
use super::{ use super::{
BlocksTile, CombatStats, Consumable, Item, Monster, Name, Player, Position, ProvidesHealing, Rect, Renderable, BlocksTile, CombatStats, Consumable, InflictsDamage, Item, Monster, Name, Player, Position, ProvidesHealing,
Viewshed, MAPWIDTH, Ranged, Rect, Renderable, Viewshed, AOE, MAPWIDTH,
}; };
use rltk::{RandomNumberGenerator, RGB}; use rltk::{RandomNumberGenerator, RGB};
use specs::prelude::*; use specs::prelude::*;
@ -45,12 +45,14 @@ pub fn random_item(ecs: &mut World, x: i32, y: i32) {
match roll { match roll {
1 => health_potion(ecs, x, y), 1 => health_potion(ecs, x, y),
2 => poison_potion(ecs, x, y), 2 => poison_potion(ecs, x, y),
3 => magic_missile_scroll(ecs, x, y),
4 => fireball_scroll(ecs, x, y),
_ => weak_health_potion(ecs, x, y), _ => weak_health_potion(ecs, x, y),
} }
} }
const MAX_MONSTERS: i32 = 4; const MAX_MONSTERS: i32 = 4;
const MAX_ITEMS: i32 = 2; const MAX_ITEMS: i32 = 6;
fn monster<S: ToString>(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharType, name: S) { fn monster<S: ToString>(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharType, name: S) {
ecs.create_entity() ecs.create_entity()
@ -138,7 +140,7 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) {
.with(Name { name: "potion of health".to_string() }) .with(Name { name: "potion of health".to_string() })
.with(Item {}) .with(Item {})
.with(Consumable {}) .with(Consumable {})
.with(ProvidesHealing { heal_amount: 12 }) .with(ProvidesHealing { amount: 12 })
.build(); .build();
} }
@ -154,7 +156,7 @@ fn weak_health_potion(ecs: &mut World, x: i32, y: i32) {
.with(Name { name: "potion of lesser health".to_string() }) .with(Name { name: "potion of lesser health".to_string() })
.with(Item {}) .with(Item {})
.with(Consumable {}) .with(Consumable {})
.with(ProvidesHealing { heal_amount: 6 }) .with(ProvidesHealing { amount: 6 })
.build(); .build();
} }
@ -170,6 +172,41 @@ fn poison_potion(ecs: &mut World, x: i32, y: i32) {
.with(Name { name: "potion of ... health?".to_string() }) .with(Name { name: "potion of ... health?".to_string() })
.with(Item {}) .with(Item {})
.with(Consumable {}) .with(Consumable {})
.with(ProvidesHealing { heal_amount: -12 }) .with(ProvidesHealing { amount: -12 })
.build();
}
fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437(')'),
fg: RGB::named(rltk::BLUE),
bg: RGB::named(rltk::BLACK),
render_order: 2,
})
.with(Name { name: "scroll of magic missile".to_string() })
.with(Item {})
.with(Consumable {})
.with(Ranged { range: 12 })
.with(InflictsDamage { amount: 10 })
.build();
}
fn fireball_scroll(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position { x, y })
.with(Renderable {
glyph: rltk::to_cp437(')'),
fg: RGB::named(rltk::ORANGE),
bg: RGB::named(rltk::BLACK),
render_order: 2,
})
.with(Name { name: "scroll of fireball".to_string() })
.with(Item {})
.with(Consumable {})
.with(Ranged { range: 10 })
.with(InflictsDamage { amount: 20 })
.with(AOE { radius: 3 })
.build(); .build();
} }