From 64caf0dc1ae453147e6935697b75dea750410061 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 30 Aug 2023 09:15:45 +0100 Subject: [PATCH] less blocking - targets will try to path to any space around their tar --- docs/combat_system.txt | 1 + raws/mobs.json | 8 ++-- src/ai/approach_ai_system.rs | 49 +++++++++++++++++-- src/ai/chase_ai_system.rs | 27 +++++++++-- src/components.rs | 5 ++ src/effects/damage.rs | 85 ++++++++++++++++++++------------- src/effects/mod.rs | 9 ++-- src/effects/triggers.rs | 31 +++++++----- src/main.rs | 1 + src/map/mod.rs | 10 ++-- src/map/themes.rs | 4 +- src/map_builders/mod.rs | 4 ++ src/map_builders/room_themer.rs | 78 ++++++++++++++++++++++++++++++ src/player.rs | 1 + src/raws/rawmaster.rs | 4 ++ src/spawner.rs | 3 ++ 16 files changed, 252 insertions(+), 68 deletions(-) create mode 100644 src/map_builders/room_themer.rs diff --git a/docs/combat_system.txt b/docs/combat_system.txt index af82bc7..13a780b 100644 --- a/docs/combat_system.txt +++ b/docs/combat_system.txt @@ -46,5 +46,6 @@ Complex example, with negative AC: - At worst (AC rolls a 14), the monster must roll less than -1 to hit you. Impossible. - It rolls a 9, and your AC rolls a 2. 9 is less than 11 (10 + 3 - 2), so it hits. - It rolls 1d8 for damage, and gets a 6. +bloodstains: if starts on bloodied tile, remove blood + heal, gain xp, grow (little dog -> dog), etc. - You have negative AC, so you roll 1d14 for damage reduction, and get an 8. - The total damage is 6 - 8 = -2, but damage can't be negative, so you take 1 point of damage. diff --git a/raws/mobs.json b/raws/mobs.json index 6bb18bd..ab11ce0 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -414,7 +414,7 @@ { "id": "ogre", "name": "ogre", - "renderable": { "glyph": "O", "fg": "#10570d", "bg": "#000000", "order": 1 }, + "renderable": { "glyph": "O", "fg": "#10A70d", "bg": "#000000", "order": 1 }, "flags": ["SMALL_GROUP"], "level": 5, "bac": 5, @@ -426,11 +426,11 @@ { "id": "treant_small", "name": "treant sapling", - "renderable": { "glyph": "♠️", "fg": "#00FF00", "bg": "#000000", "order": 1 }, - "flags": ["LARGE_GROUP"], + "renderable": { "glyph": "♠️", "fg": "#10570d", "bg": "#000000", "order": 1 }, + "flags": ["LARGE_GROUP", "GREEN_BLOOD"], "level": 2, "bac": 12, - "speed": 4, + "speed": 12, "vision_range": 16, "attacks": [{ "name": "lashes", "hit_bonus": 4, "damage": "1d8" }], "loot": { "table": "scrolls", "chance": 0.05 } diff --git a/src/ai/approach_ai_system.rs b/src/ai/approach_ai_system.rs index f303e03..18bc43d 100644 --- a/src/ai/approach_ai_system.rs +++ b/src/ai/approach_ai_system.rs @@ -7,6 +7,7 @@ pub struct ApproachAI {} impl<'a> System<'a> for ApproachAI { #[allow(clippy::type_complexity)] type SystemData = ( + WriteExpect<'a, RandomNumberGenerator>, WriteStorage<'a, TakingTurn>, WriteStorage<'a, WantsToApproach>, WriteStorage<'a, Position>, @@ -19,6 +20,7 @@ impl<'a> System<'a> for ApproachAI { fn run(&mut self, data: Self::SystemData) { let ( + mut rng, mut turns, mut wants_to_approach, mut positions, @@ -37,11 +39,26 @@ impl<'a> System<'a> for ApproachAI { &turns, ).join() { turn_done.push(entity); - let path = a_star_search( - map.xy_idx(pos.x, pos.y) as i32, - map.xy_idx(approach.idx % map.width, approach.idx / map.width) as i32, - &mut *map - ); + let target_idxs = if let Some(paths) = get_adjacent_unblocked(&map, approach.idx as usize) { + paths + } else { + continue; + }; + let mut path: Option = None; + let idx = map.xy_idx(pos.x, pos.y); + for tar_idx in target_idxs { + let potential_path = rltk::a_star_search(idx, tar_idx, &mut *map); + if potential_path.success && potential_path.steps.len() > 1 { + if path.is_none() || potential_path.steps.len() < path.as_ref().unwrap().steps.len() { + path = Some(potential_path); + } + } + } + let path = if path.is_some() { + path.unwrap() + } else { + continue; + }; if path.success && path.steps.len() > 1 { let idx = map.xy_idx(pos.x, pos.y); pos.x = (path.steps[1] as i32) % map.width; @@ -61,3 +78,25 @@ impl<'a> System<'a> for ApproachAI { } } } + +/// Try to get an unblocked index within one tile of a given idx, or None. +pub fn get_adjacent_unblocked(map: &WriteExpect, idx: usize) -> Option> { + let mut adjacent = Vec::new(); + let x = (idx as i32) % map.width; + let y = (idx as i32) / map.width; + for i in -1..2 { + for j in -1..2 { + if i == 0 && j == 0 { + continue; + } + let new_idx = (x + i + (y + j) * map.width) as usize; + if !crate::spatial::is_blocked(new_idx) { + adjacent.push(new_idx); + } + } + } + if adjacent.is_empty() { + return None; + } + return Some(adjacent); +} diff --git a/src/ai/chase_ai_system.rs b/src/ai/chase_ai_system.rs index cb7942f..faf89d4 100644 --- a/src/ai/chase_ai_system.rs +++ b/src/ai/chase_ai_system.rs @@ -2,6 +2,7 @@ use crate::{ Chasing, EntityMoved, Map, Position, TakingTurn, Telepath, Viewshed use rltk::prelude::*; use specs::prelude::*; use std::collections::HashMap; +use super::approach_ai_system::get_adjacent_unblocked; // If the target is beyond this distance, they're no longer being detected, // so stop following them. This is essentially a combined value of the sound @@ -57,11 +58,27 @@ impl<'a> System<'a> for ChaseAI { ).join() { turn_done.push(entity); let target_pos = targets[&entity]; - let path = a_star_search( - map.xy_idx(pos.x, pos.y) as i32, - map.xy_idx(target_pos.0, target_pos.1) as i32, - &mut *map - ); + let target_idx = map.xy_idx(target_pos.0, target_pos.1); + let target_idxs = if let Some(paths) = get_adjacent_unblocked(&map, target_idx) { + paths + } else { + continue; + }; + let mut path: Option = None; + let idx = map.xy_idx(pos.x, pos.y); + for tar_idx in target_idxs { + let potential_path = rltk::a_star_search(idx, tar_idx, &mut *map); + if potential_path.success && potential_path.steps.len() > 1 { + if path.is_none() || potential_path.steps.len() < path.as_ref().unwrap().steps.len() { + path = Some(potential_path); + } + } + } + let path = if path.is_some() { + path.unwrap() + } else { + continue; + }; if path.success && path.steps.len() > 1 && path.steps.len() < MAX_CHASE_DISTANCE { let idx = map.xy_idx(pos.x, pos.y); pos.x = (path.steps[1] as i32) % map.width; diff --git a/src/components.rs b/src/components.rs index b08b5e1..f331df0 100644 --- a/src/components.rs +++ b/src/components.rs @@ -46,6 +46,11 @@ pub struct Renderable { pub render_order: i32, } +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Bleeds { + pub colour: RGB, +} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Player {} diff --git a/src/effects/damage.rs b/src/effects/damage.rs index f7662ab..9927891 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -13,6 +13,7 @@ use crate::{ Blind, HungerClock, HungerState, + Bleeds, }; use crate::gui::with_article; use crate::data::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME }; @@ -28,7 +29,10 @@ pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { if !target_pool.god { if let EffectType::Damage { amount } = damage.effect_type { target_pool.hit_points.current -= amount; - add_effect(None, EffectType::Bloodstain, Targets::Entity { target }); + let bleeders = ecs.read_storage::(); + if let Some(bleeds) = bleeders.get(target) { + add_effect(None, EffectType::Bloodstain { colour: bleeds.colour }, Targets::Entity { target }); + } add_effect( None, EffectType::Particle { @@ -85,44 +89,61 @@ pub fn add_confusion(ecs: &mut World, effect: &EffectSpawner, target: Entity) { } } -pub fn bloodstain(ecs: &mut World, target: usize) { +pub fn bloodstain(ecs: &mut World, target: usize, colour: RGB) { let mut map = ecs.fetch_mut::(); // If the current tile isn't bloody, bloody it. - if !map.bloodstains.contains(&target) { - map.bloodstains.insert(target); + if !map.bloodstains.contains_key(&target) { + map.bloodstains.insert(target, colour); return; } - let mut spread: i32 = target as i32; - let mut attempts: i32 = 0; - // Otherwise, roll to move one tile in any direction. - // If this tile isn't bloody, bloody it. If not, loop. - loop { - let mut rng = ecs.write_resource::(); - attempts += 1; - spread = match rng.roll_dice(1, 8) { - 1 => spread + 1, - 2 => spread - 1, - 3 => spread + 1 + map.width, - 4 => spread - 1 + map.width, - 5 => spread + 1 - map.width, - 6 => spread - 1 - map.width, - 7 => spread + map.width, - _ => spread - map.width, - }; - // - If we're in bounds and the tile is unbloodied, bloody it and return. - // - If we ever leave bounds, return. - // - Roll a dice on each failed attempt, with an increasing change to return (soft-capping max spread) - if spread > 0 && spread < map.height * map.width { - if !map.bloodstains.contains(&(spread as usize)) { - map.bloodstains.insert(spread as usize); + if map.bloodstains.get(&target).unwrap() == &colour { + let mut spread: i32 = target as i32; + let mut attempts: i32 = 0; + // Otherwise, roll to move one tile in any direction. + // If this tile isn't bloody, bloody it. If not, loop. + loop { + let mut rng = ecs.write_resource::(); + attempts += 1; + spread = match rng.roll_dice(1, 8) { + 1 => spread + 1, + 2 => spread - 1, + 3 => spread + 1 + map.width, + 4 => spread - 1 + map.width, + 5 => spread + 1 - map.width, + 6 => spread - 1 - map.width, + 7 => spread + map.width, + _ => spread - map.width, + }; + // - If we're in bounds and the tile is unbloodied, bloody it and return. + // - If we ever leave bounds, return. + // - Roll a dice on each failed attempt, with an increasing change to return (soft-capping max spread) + if spread > 0 && spread < map.height * map.width { + if !map.bloodstains.contains_key(&(spread as usize)) { + map.bloodstains.insert(spread as usize, colour); + return; + } + // If bloodied with the same colour, return + if map.bloodstains.get(&(spread as usize)).unwrap() == &colour { + if rng.roll_dice(1, 10 - attempts) == 1 { + return; + } + // If bloodied but a *different* colour, lerp this blood and current blood. + } else { + let new_col = map.bloodstains + .get(&(spread as usize)) + .unwrap() + .lerp(colour, 0.5); + map.bloodstains.insert(spread as usize, new_col); + } + } else { return; } - if rng.roll_dice(1, 10 - attempts) == 1 { - return; - } - } else { - return; } + } else { + let curr_blood = map.bloodstains.get(&target).unwrap(); + let new_colour = curr_blood.lerp(colour, 0.5); + map.bloodstains.insert(target, new_colour); + return; } } diff --git a/src/effects/mod.rs b/src/effects/mod.rs index 5341435..798943c 100644 --- a/src/effects/mod.rs +++ b/src/effects/mod.rs @@ -32,7 +32,9 @@ pub enum EffectType { Confusion { turns: i32, }, - Bloodstain, + Bloodstain { + colour: RGB, + }, Particle { glyph: FontCharType, fg: RGB, @@ -134,7 +136,6 @@ fn affect_tile(ecs: &mut World, effect: &EffectSpawner, target: usize) { } match &effect.effect_type { - EffectType::Bloodstain => damage::bloodstain(ecs, target), EffectType::Particle { .. } => particles::particle_to_tile(ecs, target as i32, &effect), _ => {} } @@ -158,9 +159,9 @@ fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target), EffectType::Healing { .. } => damage::heal_damage(ecs, effect, target), EffectType::Confusion { .. } => damage::add_confusion(ecs, effect, target), - EffectType::Bloodstain { .. } => { + EffectType::Bloodstain { colour } => { if let Some(pos) = targeting::entity_position(ecs, target) { - damage::bloodstain(ecs, pos) + damage::bloodstain(ecs, pos, *colour); } } EffectType::Particle { .. } => { diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index 3a079fd..0cbc3fa 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -32,6 +32,8 @@ use crate::{ GrantsSpell, KnownSpell, KnownSpells, + Position, + Viewshed, }; use crate::data::messages::*; use rltk::prelude::*; @@ -219,30 +221,37 @@ fn handle_damage(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Lo continue; } let renderables = ecs.read_storage::(); + let positions = ecs.read_storage::(); + let target_pos = positions.get(target).unwrap_or(&(Position { x: 0, y: 0 })); + let viewsheds = ecs.read_storage::(); + let player_viewshed = viewsheds.get(*ecs.fetch::()).unwrap(); if ecs.read_storage::().get(target).is_some() { logger = logger .colour(renderable_colour(&renderables, target)) .append("You") .colour(WHITE) .append(DAMAGE_PLAYER_HIT); - } else if ecs.read_storage::().get(target).is_some() { - if ecs.read_storage::().get(target).is_some() { + event.log = true; + } else if player_viewshed.visible_tiles.contains(&Point::new(target_pos.x, target_pos.y)) { + if ecs.read_storage::().get(target).is_some() { + if ecs.read_storage::().get(target).is_some() { + logger = logger + .append("The") + .colour(renderable_colour(&renderables, target)) + .append(obfuscate_name_ecs(ecs, target).0) + .colour(WHITE) + .append(DAMAGE_ITEM_HIT); + } + } else { logger = logger .append("The") .colour(renderable_colour(&renderables, target)) .append(obfuscate_name_ecs(ecs, target).0) .colour(WHITE) - .append(DAMAGE_ITEM_HIT); + .append(DAMAGE_OTHER_HIT); } - } else { - logger = logger - .append("The") - .colour(renderable_colour(&renderables, target)) - .append(obfuscate_name_ecs(ecs, target).0) - .colour(WHITE) - .append(DAMAGE_OTHER_HIT); + event.log = true; } - event.log = true; } return (logger, true); } diff --git a/src/main.rs b/src/main.rs index 6f6dc79..df00bdb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -729,6 +729,7 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/map/mod.rs b/src/map/mod.rs index 0444c7a..0fcd0b4 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -1,6 +1,6 @@ -use rltk::{ Algorithm2D, BaseMap, Point }; +use rltk::prelude::*; use serde::{ Deserialize, Serialize }; -use std::collections::HashSet; +use std::collections::{ HashSet, HashMap }; mod tiletype; pub use tiletype::{ tile_cost, tile_opaque, tile_walkable, TileType, get_dest, Destination }; mod interval_spawning_system; @@ -25,13 +25,13 @@ pub struct Map { pub lit_tiles: Vec, pub telepath_tiles: Vec, pub colour_offset: Vec<((f32, f32, f32), (f32, f32, f32))>, - pub additional_fg_offset: rltk::RGB, + pub additional_fg_offset: RGB, pub id: i32, pub name: String, pub short_name: String, pub depth: i32, pub difficulty: i32, - pub bloodstains: HashSet, + pub bloodstains: HashMap, pub view_blocked: HashSet, } @@ -72,7 +72,7 @@ impl Map { short_name: short_name.to_string(), depth: depth, difficulty: difficulty, - bloodstains: HashSet::new(), + bloodstains: HashMap::new(), view_blocked: HashSet::new(), }; diff --git a/src/map/themes.rs b/src/map/themes.rs index 01592cb..4cfb5c9 100644 --- a/src/map/themes.rs +++ b/src/map/themes.rs @@ -287,8 +287,8 @@ fn apply_colour_offset(mut rgb: RGB, map: &Map, idx: usize, offset: (i32, i32, i } fn apply_bloodstain_if_necessary(mut bg: RGB, map: &Map, idx: usize) -> RGB { - if map.bloodstains.contains(&idx) { - bg = bg.add(RGB::named(BLOODSTAIN_COLOUR)); + if map.bloodstains.contains_key(&idx) { + bg = bg.add(map.bloodstains[&idx]); } return bg; } diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index b35018b..763ce07 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -67,6 +67,8 @@ mod forest; use forest::forest_builder; mod foliage; use foliage::Foliage; +mod room_themer; +use room_themer::{ Theme, ThemeRooms }; // Shared data to be passed around build chain pub struct BuilderMap { @@ -284,6 +286,8 @@ fn random_room_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut Buil 1 => builder.with(RoomBasedSpawner::new()), _ => builder.with(VoronoiSpawning::new()), } + + builder.with(ThemeRooms::grass(10)); } fn random_shape_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut BuilderChain, end: bool) -> bool { diff --git a/src/map_builders/room_themer.rs b/src/map_builders/room_themer.rs new file mode 100644 index 0000000..c6edff6 --- /dev/null +++ b/src/map_builders/room_themer.rs @@ -0,0 +1,78 @@ +use super::{ BuilderMap, MetaMapBuilder, Rect, TileType }; +use crate::tile_walkable; +use rltk::RandomNumberGenerator; + +pub enum Theme { + Grass, + Forest, +} + +pub struct ThemeRooms { + pub theme: Theme, + pub percent: i32, +} + +impl MetaMapBuilder for ThemeRooms { + fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { + self.build(rng, build_data); + } +} + +impl ThemeRooms { + #[allow(dead_code)] + pub fn grass(percent: i32) -> Box { + return Box::new(ThemeRooms { theme: Theme::Grass, percent }); + } + pub fn forest(percent: i32) -> Box { + return Box::new(ThemeRooms { theme: Theme::Forest, percent }); + } + + fn grassify(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap, room: &Rect) { + let (var_x, var_y) = (rng.roll_dice(1, 3), rng.roll_dice(1, 3)); + let x1 = if room.x1 - var_x > 0 { room.x1 - var_x } else { room.x1 }; + let x2 = if room.x2 + var_x < build_data.map.width - 1 { room.x2 + var_x } else { room.x2 }; + let y1 = if room.y1 - var_y > 0 { room.y1 - var_y } else { room.y1 }; + let y2 = if room.y2 + var_y < build_data.map.height - 1 { room.y2 + var_y } else { room.y2 }; + for x in x1..x2 { + for y in y1..y2 { + let idx = build_data.map.xy_idx(x, y); + if tile_walkable(build_data.map.tiles[idx]) && build_data.map.tiles[idx] != TileType::DownStair { + let tar = if x < room.x1 || x > room.x2 || y < room.y1 || y > room.y2 { 45 } else { 90 }; + if rng.roll_dice(1, 100) <= tar { + match rng.roll_dice(1, 6) { + 1..=4 => { + build_data.map.tiles[idx] = TileType::Grass; + } + 5 => { + build_data.map.tiles[idx] = TileType::Foliage; + } + _ => { + build_data.map.tiles[idx] = TileType::HeavyFoliage; + build_data.spawn_list.push((idx, "treant_small".to_string())); + } + } + } + } + } + } + } + + fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + let rooms: Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("RoomCornerRounding requires a builder with rooms."); + } + + for room in rooms.iter() { + if rng.roll_dice(1, 100) < self.percent { + match self.theme { + Theme::Grass => self.grassify(rng, build_data, room), + _ => {} + } + build_data.take_snapshot(); + } + } + } +} diff --git a/src/player.rs b/src/player.rs index 658dfd8..f540af8 100644 --- a/src/player.rs +++ b/src/player.rs @@ -33,6 +33,7 @@ use super::{ WantsToPickupItem, get_dest, Destination, + Bleeds, }; use rltk::prelude::*; use rltk::{ Point, RandomNumberGenerator, Rltk, VirtualKeyCode }; diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 2483a88..63f06b3 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -4,6 +4,7 @@ use crate::gamesystem::*; use crate::gui::Ancestry; use crate::random_table::RandomTable; use crate::config::CONFIG; +use crate::data::visuals::BLOODSTAIN_COLOUR; use regex::Regex; use rltk::prelude::*; use specs::prelude::*; @@ -91,6 +92,8 @@ macro_rules! apply_flags { "STATIC" => $eb = $eb.with(MoveMode { mode: Movement::Static }), "RANDOM_PATH" => $eb = $eb.with(MoveMode { mode: Movement::RandomWaypoint { path: None } }), // --- RANDOM MOB ATTRIBUTES --- + "GREEN_BLOOD" => $eb = $eb.with(Bleeds { colour: RGB::named((0, 153, 0)) }), + "BLUE_BLOOD" => $eb = $eb.with(Bleeds { colour: RGB::named((0, 0, 153)) }), "SMALL_GROUP" => {} // These flags are for region spawning, "LARGE_GROUP" => {} // and don't need to apply a component. "MULTIATTACK" => $eb = $eb.with(MultiAttack {}), @@ -386,6 +389,7 @@ pub fn spawn_named_mob( eb = eb.with(BlocksTile {}); eb = eb.with(Faction { name: "hostile".to_string() }); eb = eb.with(MoveMode { mode: Movement::Random }); + eb = eb.with(Bleeds { colour: RGB::named(BLOODSTAIN_COLOUR) }); let mut xp_value = 1; let mut has_mind = true; if let Some(flags) = &mob_template.flags { diff --git a/src/spawner.rs b/src/spawner.rs index 7b8ec59..aa56afb 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -24,8 +24,10 @@ use super::{ TileType, Viewshed, BlocksTile, + Bleeds, }; use crate::data::entity; +use crate::data::visuals::BLOODSTAIN_COLOUR; use crate::gamesystem::*; use rltk::{ RandomNumberGenerator, RGB }; use specs::prelude::*; @@ -51,6 +53,7 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { bg: RGB::named(rltk::BLACK), render_order: 0, }) + .with(Bleeds { colour: RGB::named(BLOODSTAIN_COLOUR) }) .with(Player {}) .with(Mind {}) .with(Faction { name: "player".to_string() })