diff --git a/src/ai/adjacent_ai_system.rs b/src/ai/adjacent_ai_system.rs index 79ab337..eee44ab 100644 --- a/src/ai/adjacent_ai_system.rs +++ b/src/ai/adjacent_ai_system.rs @@ -1,6 +1,6 @@ use ::specs::prelude::*; -use crate::components::{Faction, MyTurn, Position, WantsToMelee}; +use crate::components::{Faction, MyTurn, Position, TileSize, WantsToMelee}; use crate::raws::{self, Reaction, RAWS}; use crate::{spatial, Map}; @@ -16,10 +16,11 @@ impl<'a> System<'a> for AdjacentAI { WriteStorage<'a, WantsToMelee>, Entities<'a>, ReadExpect<'a, Entity>, + ReadStorage<'a, TileSize>, ); fn run(&mut self, data: Self::SystemData) { - let (mut turns, factions, positions, map, mut want_melee, entities, player) = data; + let (mut turns, factions, positions, map, mut want_melee, entities, player, sizes) = data; let mut turn_done: Vec = Vec::new(); for (entity, _turn, my_faction, pos) in (&entities, &turns, &factions, &positions).join() { @@ -29,66 +30,88 @@ impl<'a> System<'a> for AdjacentAI { let w = map.width; let h = map.height; - // Add possible reactions to adjacents for each direction - if pos.x > 0 { - evaluate(idx - 1, &map, &factions, &my_faction.name, &mut reactions); - } - if pos.x < w - 1 { - evaluate(idx + 1, &map, &factions, &my_faction.name, &mut reactions); - } - if pos.y > 0 { - evaluate( - idx - w as usize, - &map, - &factions, - &my_faction.name, - &mut reactions, - ); - } - if pos.y < h - 1 { - evaluate( - idx + w as usize, - &map, - &factions, - &my_faction.name, - &mut reactions, - ); - } - if pos.y > 0 && pos.x > 0 { - evaluate( - (idx - w as usize) - 1, - &map, - &factions, - &my_faction.name, - &mut reactions, - ); - } - if pos.y > 0 && pos.x < w - 1 { - evaluate( - (idx - w as usize) + 1, - &map, - &factions, - &my_faction.name, - &mut reactions, - ); - } - if pos.y < h - 1 && pos.x > 0 { - evaluate( - (idx + w as usize) - 1, - &map, - &factions, - &my_faction.name, - &mut reactions, - ); - } - if pos.y < h - 1 && pos.x < w - 1 { - evaluate( - (idx + w as usize) + 1, - &map, - &factions, - &my_faction.name, - &mut reactions, - ); + if let Some(size) = sizes.get(entity) { + use crate::rect::Rect; + let mob_rect = Rect::new(pos.x, pos.y, size.x, size.y).get_all_tiles(); + let parent_rect = Rect::new(pos.x - 1, pos.y - 1, size.x + 2, size.y + 2); + parent_rect + .get_all_tiles() + .iter() + .filter(|t| !mob_rect.contains(t)) + .for_each(|t| { + if t.0 > 0 && t.0 < w - 1 && t.1 > 0 && t.1 < h - 1 { + let target_idx = map.xy_idx(t.0, t.1); + evaluate( + target_idx, + &map, + &factions, + &my_faction.name, + &mut reactions, + ); + } + }) + } else { + // Add possible reactions to adjacents for each direction + if pos.x > 0 { + evaluate(idx - 1, &map, &factions, &my_faction.name, &mut reactions); + } + if pos.x < w - 1 { + evaluate(idx + 1, &map, &factions, &my_faction.name, &mut reactions); + } + if pos.y > 0 { + evaluate( + idx - w as usize, + &map, + &factions, + &my_faction.name, + &mut reactions, + ); + } + if pos.y < h - 1 { + evaluate( + idx + w as usize, + &map, + &factions, + &my_faction.name, + &mut reactions, + ); + } + if pos.y > 0 && pos.x > 0 { + evaluate( + (idx - w as usize) - 1, + &map, + &factions, + &my_faction.name, + &mut reactions, + ); + } + if pos.y > 0 && pos.x < w - 1 { + evaluate( + (idx - w as usize) + 1, + &map, + &factions, + &my_faction.name, + &mut reactions, + ); + } + if pos.y < h - 1 && pos.x > 0 { + evaluate( + (idx + w as usize) - 1, + &map, + &factions, + &my_faction.name, + &mut reactions, + ); + } + if pos.y < h - 1 && pos.x < w - 1 { + evaluate( + (idx + w as usize) + 1, + &map, + &factions, + &my_faction.name, + &mut reactions, + ); + } } let mut done = false; diff --git a/src/ai/chase_ai_system.rs b/src/ai/chase_ai_system.rs index 2650beb..feef03e 100644 --- a/src/ai/chase_ai_system.rs +++ b/src/ai/chase_ai_system.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use ::specs::prelude::*; -use crate::components::{ApplyMove, Chasing, MyTurn, Position}; +use crate::components::{ApplyMove, Chasing, MyTurn, Position, TileSize}; use crate::Map; pub struct ChaseAI {} @@ -16,10 +16,13 @@ impl<'a> System<'a> for ChaseAI { ReadExpect<'a, Map>, Entities<'a>, WriteStorage<'a, ApplyMove>, + ReadStorage<'a, TileSize>, ); fn run(&mut self, data: Self::SystemData) { - let (mut turns, mut chasing, positions, map, entities, mut apply_move) = data; + use ::rltk::a_star_search; + + let (mut turns, mut chasing, positions, map, entities, mut apply_move, sizes) = data; let mut targets: HashMap = HashMap::new(); let mut end_chase: Vec = Vec::new(); @@ -40,11 +43,23 @@ impl<'a> System<'a> for ChaseAI { for (entity, pos, _chase, _myturn) in (&entities, &positions, &chasing, &turns).join() { turn_done.push(entity); let target_pos = targets[&entity]; - let path = ::rltk::a_star_search( - map.xy_idx(pos.x, pos.y) as i32, - map.xy_idx(target_pos.0, target_pos.1) as i32, - &*map, - ); + let path; + + if let Some(size) = sizes.get(entity) { + let mut map_copy = map.clone(); + map_copy.populate_blocked_multi(size.x, size.y); + path = a_star_search( + map_copy.xy_idx(pos.x, pos.y) as i32, + map_copy.xy_idx(target_pos.0, target_pos.1) as i32, + &map_copy, + ); + } else { + path = a_star_search( + map.xy_idx(pos.x, pos.y) as i32, + map.xy_idx(target_pos.0, target_pos.1) as i32, + &*map, + ); + } if path.success && path.steps.len() > 1 && path.steps.len() < 15 { apply_move diff --git a/src/camera.rs b/src/camera.rs index 4437f04..54b6d72 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -2,8 +2,9 @@ use ::rltk::{Point, Rltk}; use ::specs::prelude::*; +use crate::components::{Hidden, Position, Renderable, TileSize}; use crate::map::tile_glyph; -use crate::{colors, Hidden, Map, Position, Renderable}; +use crate::{colors, Map}; /// Whether to render an outline of the current map's boundaries const SHOW_BOUNDARIES: bool = false; @@ -58,28 +59,57 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) { let renderables = ecs.read_storage::(); let hidden = ecs.read_storage::(); let map = ecs.fetch::(); + let sizes = ecs.read_storage::(); + let entities = ecs.entities(); - let mut data = (&positions, &renderables, !&hidden) + let mut data = (&positions, &renderables, &entities, !&hidden) .join() .collect::>(); data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); - for (pos, render, _hidden) in data.iter() { - let idx = map.xy_idx(pos.x, pos.y); - if map.visible_tiles[idx] { - let entity_screen_x = pos.x - min_x; - let entity_screen_y = pos.y - min_y; - if entity_screen_x > 0 - && entity_screen_x < map_width - && entity_screen_y > 0 - && entity_screen_y < map_height - { - ctx.set( - entity_screen_x, - entity_screen_y, - render.fg, - render.bg, - render.glyph, - ); + for (pos, render, entity, _hidden) in data.iter() { + if let Some(size) = sizes.get(*entity) { + for cy in 0..size.y { + for cx in 0..size.x { + let tile_x = cx + pos.x; + let tile_y = cy + pos.y; + let idx = map.xy_idx(tile_x, tile_y); + if map.visible_tiles[idx] { + let entity_screen_x = (cx + pos.x) - min_x; + let entity_screen_y = (cy + pos.y) - min_y; + if entity_screen_x > 0 + && entity_screen_x < map_width + && entity_screen_y > 0 + && entity_screen_y < map_height + { + ctx.set( + entity_screen_x + 1, + entity_screen_y + 1, + render.fg, + render.bg, + render.glyph, + ); + } + } + } + } + } else { + let idx = map.xy_idx(pos.x, pos.y); + if map.visible_tiles[idx] { + let entity_screen_x = pos.x - min_x; + let entity_screen_y = pos.y - min_y; + if entity_screen_x > 0 + && entity_screen_x < map_width + && entity_screen_y > 0 + && entity_screen_y < map_height + { + ctx.set( + entity_screen_x, + entity_screen_y, + render.fg, + render.bg, + render.glyph, + ); + } } } } diff --git a/src/components.rs b/src/components.rs index 6261142..665bc62 100644 --- a/src/components.rs +++ b/src/components.rs @@ -515,3 +515,9 @@ pub struct SpecialAbility { pub struct SpecialAbilities { pub abilities: Vec, } + +#[derive(Component, ConvertSaveload, Clone)] +pub struct TileSize { + pub x: i32, + pub y: i32, +} diff --git a/src/effects.rs b/src/effects.rs index b2bf848..4dfb883 100644 --- a/src/effects.rs +++ b/src/effects.rs @@ -5,7 +5,7 @@ mod particles; mod targeting; mod triggers; -use std::collections::VecDeque; +use std::collections::{HashSet, VecDeque}; use std::sync::Mutex; use ::rltk::{FontCharType, RGB}; @@ -85,6 +85,7 @@ pub struct EffectSpawner { pub creator: Option, pub effect_type: EffectType, pub targets: Targets, + dedupe: HashSet, } /// Adds an effect to the queue @@ -93,21 +94,22 @@ pub fn add_effect(creator: Option, effect_type: EffectType, targets: Tar creator, effect_type, targets, + dedupe: HashSet::new(), }); } pub fn run_effects_queue(ecs: &mut World) { loop { let effect = EFFECT_QUEUE.lock().unwrap().pop_front(); - if let Some(effect) = effect { - target_applicator(ecs, &effect); + if let Some(mut effect) = effect { + target_applicator(ecs, &mut effect); } else { break; } } } -fn target_applicator(ecs: &mut World, effect: &EffectSpawner) { +fn target_applicator(ecs: &mut World, effect: &mut EffectSpawner) { if let EffectType::ItemUse { item } = effect.effect_type { triggers::item_trigger(effect.creator, item, &effect.targets, ecs); } else if let EffectType::SpellUse { spell } = effect.effect_type { @@ -115,7 +117,7 @@ fn target_applicator(ecs: &mut World, effect: &EffectSpawner) { } else if let EffectType::TriggerFire { trigger } = effect.effect_type { triggers::trigger(effect.creator, trigger, &effect.targets, ecs); } else { - match &effect.targets { + match &effect.targets.clone() { Targets::Tile { tile_idx } => affect_tile(ecs, effect, *tile_idx), Targets::Tiles { tiles } => tiles .iter() @@ -143,7 +145,7 @@ fn tile_effect_hits_entities(effect: &EffectType) -> bool { ) } -fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx: i32) { +fn affect_tile(ecs: &mut World, effect: &mut EffectSpawner, tile_idx: i32) { if tile_effect_hits_entities(&effect.effect_type) { spatial::for_each_tile_content(tile_idx as usize, |entity| { affect_entity(ecs, effect, entity) @@ -157,7 +159,11 @@ fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx: i32) { } } -fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { +fn affect_entity(ecs: &mut World, effect: &mut EffectSpawner, target: Entity) { + if effect.dedupe.contains(&target) { + return; + } + effect.dedupe.insert(target); match &effect.effect_type { EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), diff --git a/src/effects/damage.rs b/src/effects/damage.rs index a155cc8..a6b06cb 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -13,6 +13,11 @@ pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::(); if let Some(pool) = pools.get_mut(target) { if !pool.god_mode { + if let Some(creator) = damage.creator { + if creator == target { + return; + } + } if let EffectType::Damage { amount } = damage.effect_type { pool.hit_points.current -= amount; add_effect(None, EffectType::Bloodstain, Targets::Single { target }); diff --git a/src/gui/tooltip.rs b/src/gui/tooltip.rs index 273d895..f69510d 100644 --- a/src/gui/tooltip.rs +++ b/src/gui/tooltip.rs @@ -1,7 +1,7 @@ -use ::rltk::Rltk; +use ::rltk::{Point, Rltk}; use ::specs::prelude::*; -use crate::components::{Attributes, Duration, Hidden, Name, Pools, Position, StatusEffect}; +use crate::components::{Attributes, Duration, Hidden, Name, Pools, StatusEffect}; use crate::{camera, colors, Map}; struct Tooltip { @@ -54,21 +54,23 @@ impl Tooltip { } pub(super) fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { - use rltk::to_cp437; + use ::rltk::{to_cp437, Algorithm2D}; let (min_x, _max_x, min_y, _max_y) = camera::get_screen_bounds(ecs, ctx); let map = ecs.fetch::(); - let positions = ecs.read_storage::(); let hidden = ecs.read_storage::(); let attributes = ecs.read_storage::(); let pools = ecs.read_storage::(); - let entities = ecs.entities(); let mouse_pos = ctx.mouse_pos(); let mut mouse_map_pos = mouse_pos; mouse_map_pos.0 += min_x; mouse_map_pos.1 += min_y; + if mouse_pos.0 < 1 || mouse_pos.0 > 49 || mouse_pos.1 < 1 || mouse_pos.1 > 40 { + return; + } + if mouse_map_pos.0 >= map.width - 1 || mouse_map_pos.1 >= map.height - 1 || mouse_map_pos.0 < 1 @@ -77,68 +79,74 @@ pub(super) fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { return; } - if !map.visible_tiles[map.xy_idx(mouse_map_pos.0, mouse_map_pos.1)] { + if !map.in_bounds(Point::new(mouse_map_pos.0, mouse_map_pos.1)) { + return; + } + let mouse_idx = map.xy_idx(mouse_map_pos.0, mouse_map_pos.1); + if !map.visible_tiles[mouse_idx] { return; } let mut tip_boxes: Vec = Vec::new(); - for (entity, position, _hidden) in (&entities, &positions, !&hidden).join() { - if position.x == mouse_map_pos.0 && position.y == mouse_map_pos.1 { - let mut tip = Tooltip::new(); - tip.add(super::get_item_display_name(ecs, entity)); - - // Comment on attributes - if let Some(attr) = attributes.get(entity) { - let mut s = String::new(); - if attr.might.bonus < 0 { - s += "Weak. " - } - if attr.might.bonus > 0 { - s += "String. " - } - if attr.quickness.bonus < 0 { - s += "Clumsy. " - } - if attr.quickness.bonus > 0 { - s += "Agile. " - } - if attr.fitness.bonus < 0 { - s += "Unhealthy. " - } - if attr.fitness.bonus > 0 { - s += "Healthy. " - } - if attr.intelligence.bonus < 0 { - s += "Unintelligent. " - } - if attr.intelligence.bonus > 0 { - s += "Smart. " - } - if s.is_empty() { - s = "Quite Average".to_string(); - } - - tip.add(s); - } - - // Comment on pools - if let Some(stat) = pools.get(entity) { - tip.add(format!("Level: {}", stat.level)); - } - - // Status effects - let statuses = ecs.read_storage::(); - let durations = ecs.read_storage::(); - let names = ecs.read_storage::(); - for (status, duration, name) in (&statuses, &durations, &names).join() { - if status.target == entity { - tip.add(format!("{} ({})", name.name, duration.turns)); - } - } - - tip_boxes.push(tip); + crate::spatial::for_each_tile_content(mouse_idx, |entity| { + if hidden.get(entity).is_some() { + return; } - } + + let mut tip = Tooltip::new(); + tip.add(super::get_item_display_name(ecs, entity)); + + // Comment on attributes + if let Some(attr) = attributes.get(entity) { + let mut s = String::new(); + if attr.might.bonus < 0 { + s += "Weak. " + } + if attr.might.bonus > 0 { + s += "String. " + } + if attr.quickness.bonus < 0 { + s += "Clumsy. " + } + if attr.quickness.bonus > 0 { + s += "Agile. " + } + if attr.fitness.bonus < 0 { + s += "Unhealthy. " + } + if attr.fitness.bonus > 0 { + s += "Healthy. " + } + if attr.intelligence.bonus < 0 { + s += "Unintelligent. " + } + if attr.intelligence.bonus > 0 { + s += "Smart. " + } + if s.is_empty() { + s = "Quite Average".to_string(); + } + + tip.add(s); + } + + // Comment on pools + if let Some(stat) = pools.get(entity) { + tip.add(format!("Level: {}", stat.level)); + } + + // Status effects + let statuses = ecs.read_storage::(); + let durations = ecs.read_storage::(); + let names = ecs.read_storage::(); + for (status, duration, name) in (&statuses, &durations, &names).join() { + if status.target == entity { + tip.add(format!("{} ({})", name.name, duration.turns)); + } + } + + tip_boxes.push(tip); + }); if tip_boxes.is_empty() { return; diff --git a/src/main.rs b/src/main.rs index f6e48fb..1f42e8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,6 +145,7 @@ fn init_state() -> State { StatusEffect, TeachesSpell, TeleportTo, + TileSize, TownPortal, Vendor, Viewshed, diff --git a/src/map.rs b/src/map.rs index cab399f..9fd236d 100644 --- a/src/map.rs +++ b/src/map.rs @@ -50,6 +50,31 @@ impl Map { spatial::populate_blocked_from_map(self); } + pub fn populate_blocked_multi(&mut self, width: i32, height: i32) { + self.populate_blocked(); + for y in 1..self.height - 1 { + for x in 1..self.width - 1 { + let idx = self.xy_idx(x, y); + if !spatial::is_blocked(idx) { + for cy in 0..height { + for cx in 0..width { + let tx = x + cx; + let ty = y + cy; + if tx < self.width - 1 && ty < self.height - 1 { + let tidx = self.xy_idx(tx, ty); + if spatial::is_blocked(tidx) { + spatial::set_blocked(idx, true); + } + } else { + spatial::set_blocked(idx, true); + } + } + } + } + } + } + } + pub fn clear_content_index(&mut self) { spatial::clear(); } diff --git a/src/map_indexing_system.rs b/src/map_indexing_system.rs index 11bdaf7..1f770ce 100644 --- a/src/map_indexing_system.rs +++ b/src/map_indexing_system.rs @@ -1,21 +1,23 @@ use ::specs::prelude::*; -use crate::components::{BlocksTile, Pools, Position}; +use crate::components::{BlocksTile, Pools, Position, TileSize}; use crate::{spatial, Map}; pub struct MapIndexingSystem {} impl<'a> System<'a> for MapIndexingSystem { + #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Map>, ReadStorage<'a, Position>, ReadStorage<'a, BlocksTile>, ReadStorage<'a, Pools>, + ReadStorage<'a, TileSize>, Entities<'a>, ); fn run(&mut self, data: Self::SystemData) { - let (map, position, blockers, pools, entities) = data; + let (map, position, blockers, pools, sizes, entities) = data; spatial::clear(); spatial::populate_blocked_from_map(&*map); @@ -29,8 +31,21 @@ impl<'a> System<'a> for MapIndexingSystem { } if alive { - let idx = map.xy_idx(position.x, position.y); - spatial::index_entity(entity, idx, blockers.get(entity).is_some()); + if let Some(size) = sizes.get(entity) { + // Multi-tile + for y in position.y..position.y + size.y { + for x in position.x..position.x + size.x { + if x > 0 && x < map.width - 1 && y > 0 && y < map.height - 1 { + let idx = map.xy_idx(x, y); + spatial::index_entity(entity, idx, blockers.get(entity).is_some()); + } + } + } + } else { + // Single tile + let idx = map.xy_idx(position.x, position.y); + spatial::index_entity(entity, idx, blockers.get(entity).is_some()); + } } } } diff --git a/src/raws/item_structs.rs b/src/raws/item_structs.rs index 53a459b..9e5331c 100644 --- a/src/raws/item_structs.rs +++ b/src/raws/item_structs.rs @@ -23,6 +23,8 @@ pub struct Renderable { pub fg: String, pub bg: String, pub order: i32, + pub x_size: Option, + pub y_size: Option, } #[derive(Deserialize, Debug)] diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index ca756b0..0223845 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -497,6 +497,12 @@ pub fn spawn_named_mob( // Renderable if let Some(renderable) = &mob_template.renderable { eb = eb.with(get_renderable_component(renderable)); + if renderable.x_size.is_some() || renderable.y_size.is_some() { + eb = eb.with(TileSize { + x: renderable.x_size.unwrap_or(1), + y: renderable.y_size.unwrap_or(1), + }); + } } eb = eb.with(Name::from(&mob_template.name)); diff --git a/src/rect.rs b/src/rect.rs index e101ba1..7f8860a 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -1,4 +1,6 @@ //! Four points, one plane, 90° angles +use std::collections::HashSet; + use ::serde::{Deserialize, Serialize}; #[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] @@ -27,4 +29,16 @@ impl Rect { pub fn center(&self) -> (i32, i32) { ((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2) } + + pub fn get_all_tiles(&self) -> HashSet<(i32, i32)> { + let mut result = HashSet::new(); + + for y in self.y1..self.y2 { + for x in self.x1..self.x2 { + result.insert((x, y)); + } + } + + result + } } diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 727f105..d303bec 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -123,6 +123,7 @@ pub fn save_game(ecs: &mut World) { StatusEffect, TeachesSpell, TeleportTo, + TileSize, TownPortal, Vendor, Viewshed, @@ -254,6 +255,7 @@ pub fn load_game(ecs: &mut World) { StatusEffect, TeachesSpell, TeleportTo, + TileSize, TownPortal, Vendor, Viewshed, diff --git a/src/spatial.rs b/src/spatial.rs index 3198006..1cf82e2 100644 --- a/src/spatial.rs +++ b/src/spatial.rs @@ -60,6 +60,11 @@ pub fn is_blocked(idx: usize) -> bool { lock.blocked[idx].0 || lock.blocked[idx].1 } +pub fn set_blocked(idx: usize, blocked: bool) { + let mut lock = SPATIAL_MAP.lock().unwrap(); + lock.blocked[idx] = (lock.blocked[idx].0, blocked); +} + pub fn for_each_tile_content(idx: usize, mut f: F) where F: FnMut(Entity), @@ -84,6 +89,12 @@ where RunState::AwaitingInput } +#[allow(dead_code)] +pub fn get_tile_content_clone(idx: usize) -> Vec { + let lock = SPATIAL_MAP.lock().unwrap(); + lock.tile_content[idx].iter().map(|(e, _)| *e).collect() +} + /// Move an entity on the map from `moving_from` index to the `moving_to` index. /// This also recalculates if these two tiles are blocked based on the entities /// within these tiles.