1
0
Fork 0

Allow multi-tile sized entities

This commit is contained in:
Timothy Warren 2022-01-28 11:48:25 -05:00
parent 140531c601
commit 830b721548
15 changed files with 330 additions and 161 deletions

View File

@ -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<Entity> = 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;

View File

@ -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<Entity, (i32, i32)> = HashMap::new();
let mut end_chase: Vec<Entity> = 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

View File

@ -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::<Renderable>();
let hidden = ecs.read_storage::<Hidden>();
let map = ecs.fetch::<Map>();
let sizes = ecs.read_storage::<TileSize>();
let entities = ecs.entities();
let mut data = (&positions, &renderables, !&hidden)
let mut data = (&positions, &renderables, &entities, !&hidden)
.join()
.collect::<Vec<_>>();
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,
);
}
}
}
}

View File

@ -515,3 +515,9 @@ pub struct SpecialAbility {
pub struct SpecialAbilities {
pub abilities: Vec<SpecialAbility>,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct TileSize {
pub x: i32,
pub y: i32,
}

View File

@ -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<Entity>,
pub effect_type: EffectType,
pub targets: Targets,
dedupe: HashSet<Entity>,
}
/// Adds an effect to the queue
@ -93,21 +94,22 @@ pub fn add_effect(creator: Option<Entity>, 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),

View File

@ -13,6 +13,11 @@ pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) {
let mut pools = ecs.write_storage::<Pools>();
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 });

View File

@ -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::<Map>();
let positions = ecs.read_storage::<Position>();
let hidden = ecs.read_storage::<Hidden>();
let attributes = ecs.read_storage::<Attributes>();
let pools = ecs.read_storage::<Pools>();
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<Tooltip> = 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::<StatusEffect>();
let durations = ecs.read_storage::<Duration>();
let names = ecs.read_storage::<Name>();
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::<StatusEffect>();
let durations = ecs.read_storage::<Duration>();
let names = ecs.read_storage::<Name>();
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;

View File

@ -145,6 +145,7 @@ fn init_state() -> State {
StatusEffect,
TeachesSpell,
TeleportTo,
TileSize,
TownPortal,
Vendor,
Viewshed,

View File

@ -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();
}

View File

@ -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());
}
}
}
}

View File

@ -23,6 +23,8 @@ pub struct Renderable {
pub fg: String,
pub bg: String,
pub order: i32,
pub x_size: Option<i32>,
pub y_size: Option<i32>,
}
#[derive(Deserialize, Debug)]

View File

@ -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));

View File

@ -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
}
}

View File

@ -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,

View File

@ -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<F>(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<Entity> {
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.