ai refactor, mob spawns now take into account player level, small fixes

This commit is contained in:
Llywelwyn 2023-07-31 22:24:38 +01:00
parent c00418f7c8
commit 6cef899ef6
21 changed files with 301 additions and 148 deletions

View file

@ -44,8 +44,8 @@
"name": "priest",
"renderable": { "glyph": "@", "fg": "#FFFFFF", "bg": "#000000", "order": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"quips": ["Light's givings.", "<a quiet prayer>", "Bless you."],
"vision_range": 4
"vision_range": 4,
"quips": ["Light's givings.", "<a quiet prayer>", "Bless you."]
},
{
"id": "npc_miner",
@ -53,8 +53,8 @@
"renderable": { "glyph": "@", "fg": "#946123", "bg": "#000000", "order": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4,
"quips": ["You're not borrowing my pick."],
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }]
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }],
"quips": ["You're not borrowing my pick."]
},
{
"id": "npc_guard",
@ -63,20 +63,9 @@
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"level": 2,
"vision_range": 4,
"quips": ["You wont catch me down the mine.", "Staying out of trouble?"],
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }],
"equipped": ["equip_shortsword", "equip_body_leather"]
},
{
"id": "dog_little",
"name": "little dog",
"renderable": { "glyph": "d", "fg": "#FFFFFF", "bg": "#000000", "order": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"level": 2,
"bac": 6,
"vision_range": 12,
"quips": ["<woof!>", "<bark!>", "<grrr..>"],
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }]
"equipped": ["equip_shortsword", "equip_body_leather"],
"quips": ["You wont catch me down the mine.", "Staying out of trouble?"]
},
{
"id": "rat",
@ -86,8 +75,7 @@
"bac": 6,
"vision_range": 8,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }],
"equipped": ["equip_shortsword", "equip_body_leather"],
"loot": { "table": "scrolls", "chance": 0.05 }
"loot": { "table": "food", "chance": 0.1 }
},
{
"id": "chicken",
@ -111,7 +99,7 @@
"id": "sheep_little",
"name": "lamb",
"renderable": { "glyph": "q", "fg": "#e7e7e7", "bg": "#000000", "order": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"flags": ["BYSTANDER", "BLOCKS_TILE", "SMALL_GROUP"],
"bac": 10,
"vision_range": 4,
"attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }]
@ -132,11 +120,13 @@
"flags": ["BYSTANDER", "BLOCKS_TILE", "MULTIATTACK"],
"level": 3,
"bac": 6,
"speed": 16,
"vision_range": 8,
"attacks": [
{ "name": "kicks", "hit_bonus": 0, "damage": "1d6" },
{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }
]
],
"quips": ["<a disgruntled neigh>"]
},
{
"id": "horse",
@ -145,6 +135,7 @@
"flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"],
"level": 5,
"bac": 5,
"speed": 20,
"vision_range": 8,
"attacks": [
{ "name": "kicks", "hit_bonus": 0, "damage": "1d8" },
@ -158,6 +149,7 @@
"flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"],
"level": 7,
"bac": 4,
"speed": 24,
"vision_range": 8,
"attacks": [
{ "name": "kicks", "hit_bonus": 0, "damage": "1d10" },
@ -168,13 +160,25 @@
"id": "rat_giant",
"name": "giant rat",
"renderable": { "glyph": "r", "fg": "#bb8000", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"],
"level": 1,
"bac": 7,
"vision_range": 8,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }],
"loot": { "table": "scrolls", "chance": 0.05 }
},
{
"id": "dog_little",
"name": "little dog",
"renderable": { "glyph": "d", "fg": "#FFFFFF", "bg": "#000000", "order": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"level": 2,
"bac": 6,
"speed": 18,
"vision_range": 12,
"quips": ["<woof!>", "<bark!>", "<grrr..>"],
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }]
},
{
"id": "dog",
"name": "dog",
@ -182,6 +186,7 @@
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 4,
"bac": 5,
"speed": 16,
"vision_range": 12,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }]
},
@ -192,6 +197,7 @@
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 6,
"bac": 4,
"speed": 15,
"vision_range": 12,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }]
},
@ -201,6 +207,7 @@
"renderable": { "glyph": "g", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 1,
"speed": 9,
"vision_range": 12,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }]
},
@ -218,7 +225,7 @@
"id": "jackal",
"name": "jackal",
"renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"],
"bac": 7,
"vision_range": 12,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }]
@ -236,7 +243,7 @@
"id": "coyote",
"name": "coyote",
"renderable": { "glyph": "d", "fg": "#6E3215", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"],
"level": 1,
"bac": 7,
"vision_range": 12,
@ -258,6 +265,7 @@
"renderable": { "glyph": "G", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 2,
"speed": 9,
"vision_range": 12,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }],
"loot": { "table": "wands", "chance": 0.05 }
@ -266,29 +274,48 @@
"id": "orc",
"name": "orc",
"renderable": { "glyph": "o", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"],
"level": 1,
"speed": 9,
"vision_range": 12,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "equipment", "chance": 0.05 }
},
{
"id": "orc_large",
"name": "large orc",
"renderable": { "glyph": "o", "fg": "#008000", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"id": "orc_hill",
"name": "hill orc",
"renderable": { "glyph": "o", "fg": "#dbd830", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE", "LARGE_GROUP"],
"level": 2,
"vision_range": 12,
"speed": 9,
"vision_range": 11,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "equipment", "chance": 0.05 }
},
{
"id": "orc_captain",
"name": "orc captain",
"renderable": { "glyph": "o", "fg": "#9331ac", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"],
"level": 5,
"speed": 5,
"vision_range": 12,
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "2d4" },
{ "name": "hits", "hit_bonus": 0, "damage": "2d4" }
],
"loot": { "table": "equipment", "chance": 0.05 }
},
{
"id": "ogre",
"name": "ogre",
"renderable": { "glyph": "O", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"],
"level": 5,
"bac": 5,
"speed": 10,
"vision_range": 8,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "2d5" }],
"loot": { "table": "food", "chance": 0.05 }
}
]

View file

@ -61,6 +61,7 @@
{
"id": "mobs",
"table": [
{ "id": "sheep_little", "weight": 1, "difficulty": 0},
{ "id": "chicken", "weight": 1, "difficulty": 1},
{ "id": "rat", "weight": 1, "difficulty": 1},
{ "id": "goblin", "weight": 3, "difficulty": 1},
@ -68,19 +69,19 @@
{ "id": "fox", "weight": 1, "difficulty": 1},
{ "id": "jackal", "weight": 4, "difficulty": 1},
{ "id": "deer_little", "weight": 1, "difficulty": 1},
{ "id": "sheep_little", "weight": 1, "difficulty": 1},
{ "id": "rat_giant", "weight": 2, "difficulty": 2},
{ "id": "coyote", "weight": 4, "difficulty": 2},
{ "id": "dog_little", "weight": 1, "difficulty": 3},
{ "id": "orc", "weight": 2, "difficulty": 3},
{ "id": "orc_large", "weight": 1, "difficulty": 3},
{ "id": "orc_hill", "weight": 1, "difficulty": 4},
{ "id": "goblin_chieftain", "weight": 1, "difficulty": 3},
{ "id": "ogre", "weight": 1, "difficulty": 4},
{ "id": "horse_little", "weight": 2, "difficulty": 4},
{ "id": "dog", "weight": 1, "difficulty": 5},
{ "id": "wolf", "weight": 2, "difficulty": 6},
{ "id": "orc_captain", "weight": 1, "difficulty": 7},
{ "id": "dog_large", "weight": 1, "difficulty": 7},
{ "id": "horse", "weight": 2, "difficulty": 7},
{ "id": "ogre", "weight": 1, "difficulty": 7},
{ "id": "horse_large", "weight": 2, "difficulty": 9}
]
},
@ -89,7 +90,7 @@
"table": [
{ "id": "trap_bear", "weight": 2, "difficulty": 1},
{ "id": "trap_confusion", "weight": 1, "difficulty": 1},
{ "id": "trap_stonefall", "weight": 1, "difficulty": 3}
{ "id": "trap_stonefall", "weight": 1, "difficulty": 5}
]
}
]

View file

@ -32,6 +32,7 @@ impl<'a> System<'a> for EnergySystem {
if energy.current >= TURN_COST {
energy.current -= TURN_COST;
crate::gamelog::record_event("turns", 1);
// Handle spawning mobs each turn
if LOG_TICKS {
console::log(format!("===== TURN {} =====", crate::gamelog::get_event_count("turns")));
}

View file

@ -1,4 +1,6 @@
mod energy_system;
pub use energy_system::{EnergySystem, NORMAL_SPEED};
mod turn_status;
pub use turn_status::TurnStatusSystem;
mod turn_status_system;
pub use turn_status_system::TurnStatusSystem;
mod quip_system;
pub use quip_system::QuipSystem;

38
src/ai/quip_system.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::{gamelog, Name, Quips, TakingTurn, Viewshed};
use rltk::prelude::*;
use specs::prelude::*;
pub struct QuipSystem {}
impl<'a> System<'a> for QuipSystem {
#[allow(clippy::type_complexity)]
type SystemData = (
WriteStorage<'a, Quips>,
ReadStorage<'a, Name>,
ReadStorage<'a, TakingTurn>,
ReadExpect<'a, Point>,
ReadStorage<'a, Viewshed>,
WriteExpect<'a, RandomNumberGenerator>,
);
fn run(&mut self, data: Self::SystemData) {
let (mut quips, names, turns, player_pos, viewsheds, mut rng) = data;
for (quip, name, viewshed, _turn) in (&mut quips, &names, &viewsheds, &turns).join() {
if !quip.available.is_empty() && viewshed.visible_tiles.contains(&player_pos) && rng.roll_dice(1, 6) == 1 {
let quip_index = if quip.available.len() == 1 {
0
} else {
(rng.roll_dice(1, quip.available.len() as i32) - 1) as usize
};
gamelog::Logger::new()
.append("The")
.npc_name(&name.name)
.append_n("says \"")
.append_n(&quip.available[quip_index])
.append("\"")
.log();
quip.available.remove(quip_index);
}
}
}
}

View file

@ -1,4 +1,4 @@
use super::{gamelog, Bystander, EntityMoved, Map, Name, Point, Position, Quips, TakingTurn, Viewshed};
use super::{Bystander, EntityMoved, Map, Position, TakingTurn, Viewshed};
use specs::prelude::*;
pub struct BystanderAI {}
@ -13,54 +13,15 @@ impl<'a> System<'a> for BystanderAI {
WriteStorage<'a, Position>,
WriteStorage<'a, EntityMoved>,
WriteExpect<'a, rltk::RandomNumberGenerator>,
ReadExpect<'a, Point>,
WriteStorage<'a, Quips>,
ReadStorage<'a, Name>,
ReadStorage<'a, TakingTurn>,
);
fn run(&mut self, data: Self::SystemData) {
let (
mut map,
entities,
mut viewshed,
bystander,
mut position,
mut entity_moved,
mut rng,
player_pos,
mut quips,
names,
turns,
) = data;
let (mut map, entities, mut viewshed, bystander, mut position, mut entity_moved, mut rng, turns) = data;
for (entity, mut viewshed, _bystander, mut pos, _turn) in
(&entities, &mut viewshed, &bystander, &mut position, &turns).join()
{
// Possibly quip
let quip = quips.get_mut(entity);
if let Some(quip) = quip {
if !quip.available.is_empty()
&& viewshed.visible_tiles.contains(&player_pos)
&& rng.roll_dice(1, 20) == 1
{
let name = names.get(entity);
let quip_index = if quip.available.len() == 1 {
0
} else {
(rng.roll_dice(1, quip.available.len() as i32) - 1) as usize
};
gamelog::Logger::new()
.append("The")
.npc_name(&name.unwrap().name)
.append_n("says \"")
.append_n(&quip.available[quip_index])
.append("\"")
.log();
quip.available.remove(quip_index);
}
}
// Try to move randomly
let mut x = pos.x;
let mut y = pos.y;

View file

@ -468,7 +468,7 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Opti
1 + y_offset,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
"Drop what? [aA-zZ][Esc.]",
"Unequip what? [aA-zZ][Esc.]",
);
let mut equippable: Vec<(Entity, String)> = Vec::new();

View file

@ -1,23 +1,29 @@
use super::{camera::get_screen_bounds, Attributes, Hidden, Map, Name, Pools, Position, Rltk, World, RGB};
use super::{camera::get_screen_bounds, Attributes, Hidden, Map, Name, Pools, Position, Renderable, Rltk, World, RGB};
use rltk::prelude::*;
use specs::prelude::*;
struct Tooltip {
lines: Vec<String>,
lines: Vec<(String, RGB)>,
}
const ATTRIBUTE_COLOUR: RGB = RGB { r: 1.0, g: 0.75, b: 0.8 };
const RED_WARNING: RGB = RGB { r: 1.0, g: 0.0, b: 0.0 };
const ORANGE_WARNING: RGB = RGB { r: 1.0, g: 0.65, b: 0.0 };
const YELLOW_WARNING: RGB = RGB { r: 1.0, g: 1.0, b: 0.0 };
const GREEN_WARNING: RGB = RGB { r: 0.0, g: 1.0, b: 0.0 };
impl Tooltip {
fn new() -> Tooltip {
return Tooltip { lines: Vec::new() };
}
fn add<S: ToString>(&mut self, line: S) {
self.lines.push(line.to_string());
fn add<S: ToString>(&mut self, line: S, fg: RGB) {
self.lines.push((line.to_string(), fg));
}
fn width(&self) -> i32 {
let mut max = 0;
for s in self.lines.iter() {
if s.len() > max {
max = s.len();
if s.0.len() > max {
max = s.0.len();
}
}
return max as i32 + 2i32;
@ -26,23 +32,9 @@ impl Tooltip {
return self.lines.len() as i32 + 2i32;
}
fn render(&self, ctx: &mut Rltk, x: i32, y: i32) {
let white = RGB::named(rltk::WHITE);
let weak = RGB::named(rltk::CYAN);
let strong = RGB::named(rltk::ORANGE);
let attribute = RGB::named(rltk::PINK);
ctx.draw_box(x, y, self.width() - 1, self.height() - 1, RGB::named(WHITE), RGB::named(BLACK));
for (i, s) in self.lines.iter().enumerate() {
let col = if i == 0 {
white
} else if s.starts_with('-') {
weak
} else if s.starts_with('*') {
strong
} else {
attribute
};
ctx.print_color(x + 1, y + i as i32 + 1, col, RGB::named(BLACK), &s);
ctx.print_color(x + 1, y + i as i32 + 1, s.1, RGB::named(BLACK), &s.0);
}
}
}
@ -53,6 +45,7 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
let map = ecs.fetch::<Map>();
let names = ecs.read_storage::<Name>();
let positions = ecs.read_storage::<Position>();
let renderables = ecs.read_storage::<Renderable>();
let hidden = ecs.read_storage::<Hidden>();
let attributes = ecs.read_storage::<Attributes>();
let pools = ecs.read_storage::<Pools>();
@ -77,10 +70,10 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
}
let mut tooltips: Vec<Tooltip> = Vec::new();
for (entity, name, position, _hidden) in (&entities, &names, &positions, !&hidden).join() {
for (entity, name, position, renderable, _hidden) in (&entities, &names, &positions, &renderables, !&hidden).join() {
if position.x == mouse_pos_adjusted.0 && position.y == mouse_pos_adjusted.1 {
let mut tip = Tooltip::new();
tip.add(name.name.to_string());
tip.add(name.name.to_string(), renderable.fg);
// Attributes
let attr = attributes.get(entity);
if let Some(a) = attr {
@ -101,7 +94,7 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
if s.ends_with(" ") {
s.pop();
}
tip.add(s);
tip.add(s, ATTRIBUTE_COLOUR);
}
}
// Pools
@ -110,9 +103,19 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
if let Some(p) = pool {
let level_diff: i32 = p.level - player_pool.level;
if level_diff <= -2 {
tip.add("-weak-");
tip.add("-weak-", YELLOW_WARNING);
} else if level_diff >= 2 {
tip.add("*threatening*");
tip.add("*threatening*", ORANGE_WARNING);
}
let health_percent: f32 = p.hit_points.current as f32 / p.hit_points.max as f32;
if health_percent == 1.0 {
tip.add("healthy", GREEN_WARNING);
} else if health_percent <= 0.25 {
tip.add("*critical*", RED_WARNING);
} else if health_percent <= 0.5 {
tip.add("-bloodied-", ORANGE_WARNING);
} else if health_percent <= 0.75 {
tip.add("injured", YELLOW_WARNING);
}
}
tooltips.push(tip);

View file

@ -82,7 +82,15 @@ impl State {
self.mapgen_timer = 0.0;
self.mapgen_history.clear();
let mut rng = self.ecs.write_resource::<rltk::RandomNumberGenerator>();
let mut builder = map_builders::level_builder(new_id, &mut rng, 100, 50);
let mut player_level = 1;
{
let player = self.ecs.read_storage::<Player>();
let pools = self.ecs.read_storage::<Pools>();
for (_p, pool) in (&player, &pools).join() {
player_level = pool.level;
}
}
let mut builder = map_builders::level_builder(new_id, &mut rng, 100, 50, player_level);
builder.build_map(&mut rng);
std::mem::drop(rng);
self.mapgen_history = builder.build_data.history.clone();
@ -125,6 +133,7 @@ impl State {
let mut vis = VisibilitySystem {};
let mut energy = ai::EnergySystem {};
let mut turn_status_system = ai::TurnStatusSystem {};
let mut quip_system = ai::QuipSystem {};
let mut mob = MonsterAI {};
let mut bystanders = bystander_ai_system::BystanderAI {};
let mut trigger_system = trigger_system::TriggerSystem {};
@ -141,6 +150,7 @@ impl State {
vis.run_now(&self.ecs);
energy.run_now(&self.ecs);
turn_status_system.run_now(&self.ecs);
quip_system.run_now(&self.ecs);
mob.run_now(&self.ecs);
bystanders.run_now(&self.ecs);
trigger_system.run_now(&self.ecs);
@ -595,11 +605,11 @@ fn main() -> rltk::BError {
raws::load_raws();
gs.ecs.insert(rltk::RandomNumberGenerator::new());
gs.ecs.insert(Map::new(1, 64, 64, 0, "New Map"));
gs.ecs.insert(Point::new(0, 0));
gs.ecs.insert(Map::new(1, 64, 64, 0, "New Map")); // Map
gs.ecs.insert(Point::new(0, 0)); // Player pos
let player_entity = spawner::player(&mut gs.ecs, 0, 0);
gs.ecs.insert(player_entity);
gs.ecs.insert(RunState::MapGeneration {});
gs.ecs.insert(player_entity); // Player entity
gs.ecs.insert(RunState::MapGeneration {}); // RunState
gs.ecs.insert(particle_system::ParticleBuilder::new());
gs.ecs.insert(rex_assets::RexAssets::new());

View file

@ -10,8 +10,9 @@ pub fn forest_builder(
width: i32,
height: i32,
difficulty: i32,
initial_player_level: i32,
) -> BuilderChain {
let mut chain = BuilderChain::new(new_id, width, height, difficulty, "Into the Woods");
let mut chain = BuilderChain::new(new_id, width, height, difficulty, "Into the Woods", initial_player_level);
chain.start_with(CellularAutomataBuilder::new());
chain.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE));
chain.with(CullUnreachable::new());

View file

@ -73,6 +73,7 @@ pub struct BuilderMap {
pub history: Vec<Map>,
pub width: i32,
pub height: i32,
pub initial_player_level: i32,
}
impl BuilderMap {
@ -94,7 +95,14 @@ pub struct BuilderChain {
}
impl BuilderChain {
pub fn new<S: ToString>(new_id: i32, width: i32, height: i32, difficulty: i32, name: S) -> BuilderChain {
pub fn new<S: ToString>(
new_id: i32,
width: i32,
height: i32,
difficulty: i32,
name: S,
initial_player_level: i32,
) -> BuilderChain {
BuilderChain {
starter: None,
builders: Vec::new(),
@ -107,6 +115,7 @@ impl BuilderChain {
history: Vec::new(),
width: width,
height: height,
initial_player_level: initial_player_level,
},
}
}
@ -296,9 +305,10 @@ pub fn random_builder(
width: i32,
height: i32,
difficulty: i32,
initial_player_level: i32,
) -> BuilderChain {
rltk::console::log(format!("DEBUGINFO: Building random (ID:{}, DIFF:{})", new_id, difficulty));
let mut builder = BuilderChain::new(new_id, width, height, difficulty, "<PLACEHOLDER>");
let mut builder = BuilderChain::new(new_id, width, height, difficulty, "<PLACEHOLDER>", initial_player_level);
let type_roll = rng.roll_dice(1, 2);
let mut want_doors = true;
match type_roll {
@ -339,12 +349,18 @@ pub fn random_builder(
builder
}
pub fn level_builder(new_id: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
pub fn level_builder(
new_id: i32,
rng: &mut rltk::RandomNumberGenerator,
width: i32,
height: i32,
initial_player_level: i32,
) -> BuilderChain {
// TODO: With difficulty and ID/depth decoupled, this can be used for branches later.
let difficulty = new_id;
match new_id {
1 => town_builder(new_id, rng, width, height),
2 => forest_builder(new_id, rng, width, height, difficulty),
_ => random_builder(new_id, rng, width, height, difficulty),
1 => town_builder(new_id, rng, width, height, 0, initial_player_level),
2 => forest_builder(new_id, rng, width, height, 1, initial_player_level),
_ => random_builder(new_id, rng, width, height, difficulty, initial_player_level),
}
}

View file

@ -69,6 +69,7 @@ impl PrefabBuilder {
}
fn char_to_map(&mut self, ch: char, idx: usize, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
let difficulty = (build_data.map.difficulty + build_data.initial_player_level) / 2;
match ch {
' ' => build_data.map.tiles[idx] = TileType::Floor,
'#' => build_data.map.tiles[idx] = TileType::Wall,
@ -102,23 +103,23 @@ impl PrefabBuilder {
}
'%' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::food_table(build_data.map.difficulty).roll(rng)));
build_data.spawn_list.push((idx, spawner::food_table(difficulty).roll(rng)));
}
'!' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::potion_table(build_data.map.difficulty).roll(rng)));
build_data.spawn_list.push((idx, spawner::potion_table(difficulty).roll(rng)));
}
'/' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::wand_table(build_data.map.difficulty).roll(rng)));
build_data.spawn_list.push((idx, spawner::wand_table(difficulty).roll(rng)));
}
'?' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::scroll_table(build_data.map.difficulty).roll(rng)));
build_data.spawn_list.push((idx, spawner::scroll_table(difficulty).roll(rng)));
}
')' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::equipment_table(build_data.map.difficulty).roll(rng)));
build_data.spawn_list.push((idx, spawner::equipment_table(difficulty).roll(rng)));
}
_ => {
rltk::console::log(format!("Unknown glyph '{}' when loading prefab", (ch as u8) as char));

View file

@ -18,7 +18,13 @@ impl RoomBasedSpawner {
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
if let Some(rooms) = &build_data.rooms {
for room in rooms.iter().skip(1) {
spawner::spawn_room(&build_data.map, rng, room, &mut build_data.spawn_list);
spawner::spawn_room(
&build_data.map,
rng,
room,
&mut build_data.spawn_list,
build_data.initial_player_level,
);
}
} else {
panic!("RoomBasedSpawner only works after rooms have been created");

View file

@ -18,7 +18,13 @@ impl CorridorSpawner {
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
if let Some(corridors) = &build_data.corridors {
for corridor in corridors.iter() {
spawner::spawn_region(&build_data.map, rng, &corridor, &mut build_data.spawn_list);
spawner::spawn_region(
&build_data.map,
rng,
&corridor,
&mut build_data.spawn_list,
build_data.initial_player_level,
);
}
} else {
panic!("CorridorSpawner only works after corridors have been created");

View file

@ -1,10 +1,16 @@
use super::{BuilderChain, BuilderMap, InitialMapBuilder, Position, TileType};
use std::collections::HashSet;
pub fn town_builder(new_id: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
let difficulty = 0;
pub fn town_builder(
new_id: i32,
_rng: &mut rltk::RandomNumberGenerator,
width: i32,
height: i32,
difficulty: i32,
initial_player_level: i32,
) -> BuilderChain {
rltk::console::log(format!("DEBUGINFO: Building town (ID:{}, DIFF:{})", new_id, difficulty));
let mut chain = BuilderChain::new(new_id, width, height, difficulty, "<PLACEHOLDER>");
let mut chain = BuilderChain::new(new_id, width, height, difficulty, "<PLACEHOLDER>", initial_player_level);
chain.start_with(TownBuilder::new());
return chain;

View file

@ -42,7 +42,13 @@ impl VoronoiSpawning {
// Spawn the entities
for area in noise_areas.iter() {
spawner::spawn_region(&build_data.map, rng, area.1, &mut build_data.spawn_list);
spawner::spawn_region(
&build_data.map,
rng,
area.1,
&mut build_data.spawn_list,
build_data.initial_player_level,
);
}
}
}

View file

@ -15,9 +15,9 @@ pub struct Mob {
pub attributes: Option<MobAttributes>,
pub skills: Option<HashMap<String, i32>>,
pub vision_range: i32,
pub quips: Option<Vec<String>>,
pub equipped: Option<Vec<String>>,
pub loot: Option<LootTableInfo>,
pub quips: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]

View file

@ -469,7 +469,7 @@ pub fn table_by_name(raws: &RawMaster, key: &str, difficulty: i32) -> RandomTabl
use super::SpawnTableEntry;
let upper_bound = difficulty;
let lower_bound = difficulty / 6;
let lower_bound = if key != "mobs" { 0 } else { difficulty / 6 };
let available_options: Vec<&SpawnTableEntry> = spawn_table
.table
@ -559,3 +559,26 @@ pub fn roll_on_loot_table(raws: &RawMaster, rng: &mut RandomNumberGenerator, key
console::log(format!("DEBUGINFO: Unknown loot table {}", key));
return None;
}
#[derive(PartialEq, Copy, Clone)]
pub enum SpawnsAs {
Single,
SmallGroup,
LargeGroup,
}
pub fn check_if_mob_spawns_in_group(raws: &RawMaster, key: &str) -> SpawnsAs {
if raws.mob_index.contains_key(key) {
let mob_template = &raws.raws.mobs[raws.mob_index[key]];
if let Some(flags) = &mob_template.flags {
for flag in flags {
match flag.as_str() {
"SMALL_GROUP" => return SpawnsAs::SmallGroup,
"LARGE_GROUP" => return SpawnsAs::LargeGroup,
_ => {}
}
}
}
}
return SpawnsAs::Single;
}

View file

@ -78,7 +78,13 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
}
/// Fills a room with stuff!
pub fn spawn_room(map: &Map, rng: &mut RandomNumberGenerator, room: &Rect, spawn_list: &mut Vec<(usize, String)>) {
pub fn spawn_room(
map: &Map,
rng: &mut RandomNumberGenerator,
room: &Rect,
spawn_list: &mut Vec<(usize, String)>,
player_level: i32,
) {
let mut possible_targets: Vec<usize> = Vec::new();
{
// Borrow scope - to keep access to the map separated
@ -92,24 +98,26 @@ pub fn spawn_room(map: &Map, rng: &mut RandomNumberGenerator, room: &Rect, spawn
}
}
spawn_region(map, rng, &possible_targets, spawn_list);
spawn_region(map, rng, &possible_targets, spawn_list, player_level);
}
pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize], spawn_list: &mut Vec<(usize, String)>) {
pub fn spawn_region(
map: &Map,
rng: &mut RandomNumberGenerator,
area: &[usize],
spawn_list: &mut Vec<(usize, String)>,
player_level: i32,
) {
let mut spawn_points: HashMap<usize, String> = HashMap::new();
let mut areas: Vec<usize> = Vec::from(area);
let difficulty = map.difficulty;
let difficulty = (map.difficulty + player_level) / 2;
// If no area, log and return.
if areas.len() == 0 {
rltk::console::log("DEBUGINFO: No areas capable of spawning mobs!");
return;
}
// Get num of each entity type.
let num_mobs = match rng.roll_dice(1, 20) {
1..=4 => 1, // 20% chance of spawning 1 mob.
5 => 3, // 5% chance of spawning 3 mobs.
_ => 0, // 75% chance of spawning 0
};
let spawn_mob: bool = rng.roll_dice(1, 3) == 1;
let num_items = match rng.roll_dice(1, 20) {
1..=2 => 1, // 10% chance of spawning 1 item
3 => 2, // 5% chance of spawning 2 items
@ -122,15 +130,45 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize],
_ => 0, // 85% chance of spawning nothing
};
// Roll on each table, getting an entity + spawn point
for _i in 0..num_mobs {
entity_from_table_to_spawn_list(rng, &mut areas, mob_table(difficulty), &mut spawn_points);
if spawn_mob {
let key = mob_table(difficulty).roll(rng);
let spawn_type = raws::check_if_mob_spawns_in_group(&raws::RAWS.lock().unwrap(), &key);
let n = match spawn_type {
raws::SpawnsAs::Single => 1,
raws::SpawnsAs::SmallGroup => {
if rng.roll_dice(1, 2) == 1 {
1
} else {
4
}
}
raws::SpawnsAs::LargeGroup => {
if rng.roll_dice(1, 2) == 1 {
4
} else {
11
}
}
};
let mut roll = if n == 1 { 1 } else { rng.roll_dice(2, n) };
roll = match player_level {
0..=2 => i32::min(1, roll / 4),
3..=4 => i32::min(1, roll / 2),
_ => roll,
};
for _i in 0..roll {
entity_to_spawn_list(rng, &mut areas, key.clone(), &mut spawn_points);
}
}
for _i in 0..num_traps {
entity_from_table_to_spawn_list(rng, &mut areas, trap_table(difficulty), &mut spawn_points);
let key = trap_table(difficulty).roll(rng);
entity_to_spawn_list(rng, &mut areas, key, &mut spawn_points);
}
for _i in 0..num_items {
let spawn_table = get_random_item_category(rng, difficulty);
entity_from_table_to_spawn_list(rng, &mut areas, spawn_table, &mut spawn_points);
// Player level isn't taken into account for item spawning, to encourage
// delving deeper to gear up more quickly.
let key = get_random_item_category(rng, map.difficulty).roll(rng);
entity_to_spawn_list(rng, &mut areas, key, &mut spawn_points);
}
// Push entities and their spawn points to map's spawn list
for spawn in spawn_points.iter() {
@ -138,10 +176,10 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize],
}
}
fn entity_from_table_to_spawn_list(
fn entity_to_spawn_list(
rng: &mut RandomNumberGenerator,
possible_areas: &mut Vec<usize>,
table: RandomTable,
key: String,
spawn_points: &mut HashMap<usize, String>,
) {
if possible_areas.len() == 0 {
@ -150,7 +188,7 @@ fn entity_from_table_to_spawn_list(
let array_idx =
if possible_areas.len() == 1 { 0usize } else { (rng.roll_dice(1, possible_areas.len() as i32) - 1) as usize };
let map_idx = possible_areas[array_idx];
spawn_points.insert(map_idx, table.roll(rng));
spawn_points.insert(map_idx, key);
possible_areas.remove(array_idx);
}

View file

@ -21,6 +21,7 @@ impl<'a> System<'a> for TriggerSystem {
ReadStorage<'a, Name>,
WriteExpect<'a, ParticleBuilder>,
Entities<'a>,
WriteExpect<'a, rltk::RandomNumberGenerator>,
);
fn run(&mut self, data: Self::SystemData) {
@ -37,6 +38,7 @@ impl<'a> System<'a> for TriggerSystem {
names,
mut particle_builder,
entities,
mut rng,
) = data;
// Iterate entities that moved, and their final position
@ -60,7 +62,12 @@ impl<'a> System<'a> for TriggerSystem {
let damage = inflicts_damage.get(*entity_id);
if let Some(damage) = damage {
particle_builder.damage_taken(pos.x, pos.y);
SufferDamage::new_damage(&mut inflict_damage, entity, damage.amount, false);
SufferDamage::new_damage(
&mut inflict_damage,
entity,
rng.roll_dice(1, damage.amount),
false,
);
}
let confuses = confusion.get(*entity_id);