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)]
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)]
@ -93,6 +108,7 @@ pub struct WantsToDropItem {
#[derive(Component, Debug)]
pub struct WantsToUseItem {
pub item: Entity,
pub target: Option<rltk::Point>,
}
#[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::*;
pub struct DamageSystem {}
@ -35,6 +35,7 @@ pub fn delete_the_dead(ecs: &mut World) {
let combat_stats = ecs.read_storage::<CombatStats>();
let players = ecs.read_storage::<Player>();
let names = ecs.read_storage::<Name>();
let items = ecs.read_storage::<Item>();
let entities = ecs.entities();
let mut log = ecs.write_resource::<GameLog>();
for (entity, stats) in (&entities, &combat_stats).join() {
@ -44,7 +45,12 @@ pub fn delete_the_dead(ecs: &mut World) {
None => {
let victim_name = names.get(entity);
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)
}

View file

@ -1,6 +1,6 @@
use super::{
gamelog::GameLog, rex_assets::RexAssets, CombatStats, InBackpack, Map, Name, Player, Point, Position, RunState,
State,
State, Viewshed,
};
use rltk::{Rltk, VirtualKeyCode, RGB};
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)]
pub enum MainMenuSelection {
NewGame,

View file

@ -1,6 +1,6 @@
use super::{
gamelog::GameLog, CombatStats, Consumable, InBackpack, Name, ParticleBuilder, Position, ProvidesHealing,
WantsToDropItem, WantsToPickupItem, WantsToUseItem, DEFAULT_PARTICLE_LIFETIME,
gamelog::GameLog, CombatStats, Consumable, InBackpack, InflictsDamage, Map, Name, ParticleBuilder, Position,
ProvidesHealing, SufferDamage, WantsToDropItem, WantsToPickupItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME,
};
use specs::prelude::*;
@ -39,65 +39,156 @@ impl<'a> System<'a> for ItemUseSystem {
type SystemData = (
ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
ReadExpect<'a, Map>,
Entities<'a>,
WriteStorage<'a, WantsToUseItem>,
ReadStorage<'a, Name>,
ReadStorage<'a, Consumable>,
ReadStorage<'a, ProvidesHealing>,
WriteStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>,
ReadStorage<'a, InflictsDamage>,
ReadStorage<'a, AOE>,
);
fn run(&mut self, data: Self::SystemData) {
let (
player_entity,
mut gamelog,
map,
entities,
mut wants_use,
mut wants_to_use,
names,
consumables,
healing,
provides_healing,
mut combat_stats,
mut suffer_damage,
mut particle_builder,
positions,
inflicts_damage,
aoe,
) = data;
for (entity, use_item, stats) in (&entities, &wants_use, &mut combat_stats).join() {
let item_heals = healing.get(use_item.item);
match item_heals {
None => {}
Some(healer) => {
stats.hp = i32::min(stats.max_hp, stats.hp + healer.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,
);
}
if entity == *player_entity {
gamelog.entries.push(format!(
"You quaff the {}, and heal {} hp.",
names.get(use_item.item).unwrap().name,
healer.heal_amount
));
}
let consumable = consumables.get(use_item.item);
match consumable {
None => {}
Some(_) => {
entities.delete(use_item.item).expect("Delete failed");
for (entity, wants_to_use) in (&entities, &wants_to_use).join() {
let mut used_item = true;
let mut aoe_item = false;
let item_being_used = names.get(wants_to_use.item).unwrap();
// TARGETING
let mut targets: Vec<Entity> = Vec::new();
match wants_to_use.target {
None => {
targets.push(*player_entity);
}
Some(target) => {
let area_effect = aoe.get(wants_to_use.item);
match area_effect {
None => {
// Single target in a tile
let idx = map.xy_idx(target.x, target.y);
for mob in map.tile_content[idx].iter() {
targets.push(*mob);
}
}
Some(area_effect) => {
// AOE
aoe_item = true;
let mut blast_tiles = rltk::field_of_view(target, area_effect.radius, &*map);
blast_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1);
for tile_idx in blast_tiles.iter() {
let idx = map.xy_idx(tile_idx.x, tile_idx.y);
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,
ShowInventory,
ShowDropItem,
ShowTargeting { range: i32, item: Entity, aoe: i32 },
MainMenu { menu_selection: gui::MainMenuSelection },
}
@ -139,11 +140,28 @@ impl GameState for State {
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::<WantsToUseItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem { item: item_entity })
.expect("Unable to insert intent.");
new_runstate = RunState::PlayerTurn;
let is_ranged = self.ecs.read_storage::<Ranged>();
let ranged_item = is_ranged.get(item_entity);
if let Some(ranged_item) = ranged_item {
let is_aoe = self.ecs.read_storage::<AOE>();
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 { .. } => {
let result = gui::main_menu(self, ctx);
match result {
@ -225,6 +257,9 @@ fn main() -> rltk::BError {
gs.ecs.register::<SufferDamage>();
gs.ecs.register::<Item>();
gs.ecs.register::<ProvidesHealing>();
gs.ecs.register::<InflictsDamage>();
gs.ecs.register::<Ranged>();
gs.ecs.register::<AOE>();
gs.ecs.register::<InBackpack>();
gs.ecs.register::<WantsToPickupItem>();
gs.ecs.register::<WantsToDropItem>();
@ -245,7 +280,9 @@ fn main() -> rltk::BError {
gs.ecs.insert(map);
gs.ecs.insert(Point::new(player_x, player_y));
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(particle_system::ParticleBuilder::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;
/// 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) {
let mut dead_particles: Vec<Entity> = Vec::new();
{
@ -41,9 +46,28 @@ impl ParticleBuilder {
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) {
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 {}

View file

@ -1,6 +1,6 @@
use super::{
BlocksTile, CombatStats, Consumable, Item, Monster, Name, Player, Position, ProvidesHealing, Rect, Renderable,
Viewshed, MAPWIDTH,
BlocksTile, CombatStats, Consumable, InflictsDamage, Item, Monster, Name, Player, Position, ProvidesHealing,
Ranged, Rect, Renderable, Viewshed, AOE, MAPWIDTH,
};
use rltk::{RandomNumberGenerator, RGB};
use specs::prelude::*;
@ -45,12 +45,14 @@ pub fn random_item(ecs: &mut World, x: i32, y: i32) {
match roll {
1 => health_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),
}
}
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) {
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(Item {})
.with(Consumable {})
.with(ProvidesHealing { heal_amount: 12 })
.with(ProvidesHealing { amount: 12 })
.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(Item {})
.with(Consumable {})
.with(ProvidesHealing { heal_amount: 6 })
.with(ProvidesHealing { amount: 6 })
.build();
}
@ -170,6 +172,41 @@ fn poison_potion(ecs: &mut World, x: i32, y: i32) {
.with(Name { name: "potion of ... health?".to_string() })
.with(Item {})
.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();
}