combat system overhaul - d20/hack-like

This commit is contained in:
Llywelwyn 2023-07-28 06:29:59 +01:00
parent 32044dbb6a
commit c169a1eae6
20 changed files with 762 additions and 292 deletions

1
Cargo.lock generated
View file

@ -2278,6 +2278,7 @@ dependencies = [
"bracket-lib 0.8.7 (git+https://github.com/amethyst/bracket-lib.git?rev=851f6f08675444fb6fa088b9e67bee9fd75554c6)",
"criterion",
"lazy_static",
"regex",
"rltk",
"serde",
"serde_json",

View file

@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
rltk = { version = "^0.8.7", features = ["serde"] }
bracket-lib = { git = "https://github.com/amethyst/bracket-lib.git", rev = "851f6f08675444fb6fa088b9e67bee9fd75554c6", features = ["serde"] }
regex = "1.3.6"
specs = { version = "0.16.1", features = ["serde"] }
specs-derive = "0.4.1"
serde = { version = "1.0.93", features = ["derive"]}

View file

@ -59,43 +59,43 @@
"id": "equip_dagger",
"name": { "name": "dagger", "plural": "daggers" },
"renderable": { "glyph": ")", "fg": "#808080", "bg": "#000000", "order": 2 },
"flags": ["EQUIP_MELEE"],
"effects": { "melee_power_bonus": "1" }
"flags": ["EQUIP_MELEE", "FINESSE"],
"effects": { "base_damage": "1d4"}
},
{
"id": "equip_shortsword",
"name": { "name": "shortsword", "plural": "shortswords" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"flags": ["EQUIP_MELEE"],
"effects": { "melee_power_bonus": "2" }
"flags": ["EQUIP_MELEE", "STRENGTH"],
"effects": { "base_damage": "1d6"}
},
{
"id": "equip_longsword",
"name": { "name": "longsword", "plural": "longswords" },
"renderable": { "glyph": ")", "fg": "#FFF8DC", "bg": "#000000", "order": 2 },
"flags": ["EQUIP_MELEE"],
"effects": { "melee_power_bonus": "3" }
"flags": ["EQUIP_MELEE", "STRENGTH"],
"effects": { "base_damage": "1d8" }
},
{
"id": "equip_smallshield",
"name": { "name": "buckler", "plural": "bucklers" },
"renderable": { "glyph": "[", "fg": "#808080", "bg": "#000000", "order": 2 },
"flags": ["EQUIP_SHIELD"],
"effects": { "defence_bonus": "1" }
"effects": { "ac": "1" }
},
{
"id": "equip_mediumshield",
"name": { "name": "medium shield", "plural": "medium shields" },
"renderable": { "glyph": "[", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"flags": ["EQUIP_SHIELD"],
"effects": { "defence_bonus": "2", "melee_power_bonus": "-1" }
"effects": { "ac": "2", "melee_power_bonus": "-1" }
},
{
"id": "equip_largeshield",
"name": { "name": "large shield", "plural": "large shields" },
"renderable": { "glyph": "[", "fg": "#FFF8DC", "bg": "#000000", "order": 2 },
"flags": ["EQUIP_SHIELD"],
"effects": { "defence_bonus": "4", "melee_power_bonus": "-2" }
"effects": { "ac": "4", "melee_power_bonus": "-2" }
},
{
"id": "wand_magicmissile",

View file

@ -3,204 +3,233 @@
"id": "npc_barkeep",
"name": "barkeep",
"renderable": { "glyph": "@", "fg": "#EE82EE", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 1, "power": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4,
"ai": "bystander",
"quips": ["Drink?"]
},
{
"id": "npc_townsperson",
"name": "townsperson",
"renderable": { "glyph": "@", "fg": "#9fa86c", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4,
"ai": "bystander",
"quips": ["You won't catch me quipping."]
},
{
"id": "npc_drunk",
"name": "drunk",
"renderable": { "glyph": "@", "fg": "#a0a83c", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4,
"ai": "bystander",
"quips": ["Hic!", "H-Hic'."]
},
{
"id": "npc_fisher",
"name": "fisher",
"renderable": { "glyph": "@", "fg": "#3ca3a8", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4,
"ai": "bystander",
"quips": ["Placeholder."]
},
{
"id": "npc_dockworker",
"name": "dock worker",
"renderable": { "glyph": "@", "fg": "#68d8de", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 1, "power": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4,
"ai": "bystander",
"quips": ["Placeholder."]
},
{
"id": "npc_priest",
"name": "priest",
"renderable": { "glyph": "@", "fg": "#FFFFFF", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 },
"vision_range": 4,
"ai": "bystander"
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4
},
{
"id": "npc_miner",
"name": "miner",
"renderable": { "glyph": "@", "fg": "#946123", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"vision_range": 4,
"ai": "bystander"
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d8"}
]
},
{
"id": "npc_guard",
"name": "smalltown guard",
"renderable": { "glyph": "@", "fg": "#034efc", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 12, "hp": 12, "defence": 3, "power": 3 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"level": 2,
"vision_range": 4,
"ai": "bystander",
"quips": ["You wont catch me down the mine.", "I'm not paid enough for that."]
"quips": ["You wont catch me down the mine.", "I'm not paid enough for that."],
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d8"}
]
},
{
"id": "dog_little",
"name": "little dog",
"renderable": { "glyph": "d", "fg": "#FFFFFF", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"],
"level": 2,
"bac": 6,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "1d6"}
]
},
{
"id": "rat",
"name": "rat",
"renderable": { "glyph": "r", "fg": "#aa6000", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 4, "hp": 4, "defence": 0, "power": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"bac": 6,
"vision_range": 8,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "1d2"}
]
},
{
"id": "rat_giant",
"name": "giant rat",
"renderable": { "glyph": "r", "fg": "#bb8000", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 4, "hp": 4, "defence": 0, "power": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 2,
"bac": 7,
"vision_range": 8,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "1d3"}
]
},
{
"id": "dog",
"name": "dog",
"renderable": { "glyph": "d", "fg": "#EEEEEE", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 2 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 4,
"bac": 5,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "1d6"}
]
},
{
"id": "dog_large",
"name": "large dog",
"renderable": { "glyph": "d", "fg": "#DDDDDD", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 12, "hp": 12, "defence": 0, "power": 3 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 6,
"bac": 4,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "2d4"}
]
},
{
"id": "goblin",
"name": "goblin",
"renderable": { "glyph": "g", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 2 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d4"}
]
},
{
"id": "kobold",
"name": "kobold",
"renderable": { "glyph": "k", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"vision_range": 7,
"ai": "melee"
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d4"}
]
},
{
"id": "jackal",
"name": "jackal",
"renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"bac": 7,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "1d2"}
]
},
{
"id": "fox",
"name": "fox",
"renderable": { "glyph": "d", "fg": "#FF0000", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 4, "hp": 4, "defence": 0, "power": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"bac": 7,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "1d3"}
]
},
{
"id": "coyote",
"name": "coyote",
"renderable": { "glyph": "d", "fg": "#6E3215", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 2 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"bac": 7,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "1d4"}
]
},
{
"id": "wolf",
"name": "wolf",
"renderable": { "glyph": "d", "fg": "#5E4225", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"bac": 4,
"vision_range": 12,
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "2d4"}
]
},
{
"id": "goblin_chieftain",
"name": "goblin chieftain",
"renderable": { "glyph": "G", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 1, "power": 2 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 2,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d8"}
]
},
{
"id": "orc",
"name": "orc",
"renderable": { "glyph": "o", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 3 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d6"}
]
},
{
"id": "orc_large",
"name": "large orc",
"renderable": { "glyph": "o", "fg": "#008000", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 12, "hp": 12, "defence": 1, "power": 3 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 2,
"vision_range": 12,
"ai": "melee"
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d6"}
]
},
{
"id": "ogre",
"name": "ogre",
"renderable": { "glyph": "O", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["BLOCKS_TILE"],
"stats": { "max_hp": 24, "hp": 24, "defence": 3, "power": 3 },
"vision_range": 8,
"ai": "melee"
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 5,
"bac": 5,
"vision_range": 8
}
]

View file

@ -60,10 +60,13 @@
{ "id": "orc_large", "weight": 1, "difficulty": 3},
{ "id": "goblin_chieftain", "weight": 1, "difficulty": 3},
{ "id": "dog", "weight": 1, "difficulty": 4},
{ "id": "ogre", "weight": 1, "difficulty": 4},
{ "id": "dog_large", "weight": 1, "difficulty": 5}
{ "id": "dog", "weight": 1, "difficulty": 5},
{ "id": "wolf", "weight": 2, "difficulty": 6},
{ "id": "dog_large", "weight": 1, "difficulty": 7}
]
},
{

View file

@ -100,12 +100,19 @@ pub struct HungerClock {
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct ProvidesNutrition {}
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct CombatStats {
pub max_hp: i32,
pub hp: i32,
pub defence: i32,
pub power: i32,
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Pool {
pub max: i32,
pub current: i32,
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Pools {
pub hit_points: Pool,
pub mana: Pool,
pub xp: i32,
pub bac: i32,
pub level: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -115,6 +122,18 @@ pub struct Attribute {
pub bonus: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
pub enum Skill {
Melee,
Defence,
Magic,
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Skills {
pub skills: HashMap<Skill, i32>,
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Attributes {
pub strength: Attribute,
@ -153,15 +172,46 @@ pub struct Item {}
pub enum EquipmentSlot {
Melee,
Shield,
Head,
Neck,
Torso,
Hands,
Legs,
Feet,
}
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum WeaponAttribute {
Strength,
Dexterity,
Finesse,
}
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct MeleeWeapon {
pub attribute: WeaponAttribute,
pub damage_n_dice: i32,
pub damage_die_type: i32,
pub damage_bonus: i32,
pub hit_bonus: i32,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct NaturalAttack {
pub name: String,
pub damage_n_dice: i32,
pub damage_die_type: i32,
pub damage_bonus: i32,
pub hit_bonus: i32,
}
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct NaturalAttacks {
pub attacks: Vec<NaturalAttack>,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct MeleePowerBonus {
pub amount: i32,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct DefenceBonus {
pub struct ArmourClassBonus {
pub amount: i32,
}

View file

@ -1,11 +1,11 @@
use super::{gamelog, CombatStats, Entities, Item, Map, Name, Player, Position, RunState, SufferDamage};
use super::{gamelog, Entities, Item, Map, Name, Player, Pools, Position, RunState, SufferDamage};
use specs::prelude::*;
pub struct DamageSystem {}
impl<'a> System<'a> for DamageSystem {
type SystemData = (
WriteStorage<'a, CombatStats>,
WriteStorage<'a, Pools>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, Map>,
ReadStorage<'a, Position>,
@ -16,7 +16,7 @@ impl<'a> System<'a> for DamageSystem {
let (mut stats, mut damage, mut map, positions, entities) = data;
for (entity, mut stats, damage) in (&entities, &mut stats, &damage).join() {
stats.hp -= damage.amount.iter().sum::<i32>();
stats.hit_points.current -= damage.amount.iter().sum::<i32>();
let pos = positions.get(entity);
if let Some(pos) = pos {
let idx = map.xy_idx(pos.x, pos.y);
@ -32,13 +32,13 @@ pub fn delete_the_dead(ecs: &mut World) {
let mut dead: Vec<Entity> = Vec::new();
// Using scope to make borrow checker happy
{
let combat_stats = ecs.read_storage::<CombatStats>();
let combat_stats = ecs.read_storage::<Pools>();
let players = ecs.read_storage::<Player>();
let names = ecs.read_storage::<Name>();
let items = ecs.read_storage::<Item>();
let entities = ecs.entities();
for (entity, stats) in (&entities, &combat_stats).join() {
if stats.hp < 1 {
if stats.hit_points.current < 1 {
let player = players.get(entity);
match player {
None => {

62
src/gamesystem.rs Normal file
View file

@ -0,0 +1,62 @@
use super::{Skill, Skills};
pub fn attr_bonus(value: i32) -> i32 {
return (value - 10) / 2;
}
pub fn player_hp_per_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32) -> i32 {
return rng.roll_dice(1, 8) + attr_bonus(constitution);
}
pub fn player_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 {
let mut total = 10 + attr_bonus(constitution);
for _i in 0..level {
total += player_hp_per_level(rng, constitution);
}
return total;
}
pub fn npc_hp(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 {
let mut total = 1;
for _i in 0..level {
total += rng.roll_dice(1, 8) + attr_bonus(constitution);
}
return total;
}
pub fn mana_per_level(rng: &mut rltk::RandomNumberGenerator, intelligence: i32) -> i32 {
return rng.roll_dice(1, 4) + attr_bonus(intelligence);
}
pub fn mana_at_level(rng: &mut rltk::RandomNumberGenerator, intelligence: i32, level: i32) -> i32 {
let mut total = 12;
for _i in 0..level {
total += mana_per_level(rng, intelligence);
}
return total;
}
pub fn skill_bonus(skill: Skill, skills: &Skills) -> i32 {
if skills.skills.contains_key(&skill) {
return skills.skills[&skill];
} else {
return -4;
}
}
pub fn roll_4d6(rng: &mut rltk::RandomNumberGenerator) -> i32 {
let mut rolls: Vec<i32> = Vec::new();
for _i in 0..4 {
rolls.push(rng.roll_dice(1, 6));
}
rolls.sort_unstable();
let mut roll = 0;
rltk::console::log(format!("roll 0"));
for i in 1..rolls.len() {
roll += rolls[i];
rltk::console::log(format!("+ {}", rolls[i]));
}
return roll;
}

View file

@ -1,6 +1,6 @@
use super::{
camera, gamelog, rex_assets::RexAssets, CombatStats, Equipped, Hidden, HungerClock, HungerState, InBackpack, Map,
Name, Player, Point, Position, RunState, State, Viewshed,
camera, gamelog, gamesystem, rex_assets::RexAssets, ArmourClassBonus, Attributes, Equipped, Hidden, HungerClock,
HungerState, InBackpack, Map, Name, Player, Point, Pools, Position, RunState, Skill, Skills, State, Viewshed,
};
use rltk::{Rltk, VirtualKeyCode, RGB};
use specs::prelude::*;
@ -40,13 +40,69 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) {
ctx.draw_hollow_box(71, 0, 28, 55, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)); // Side box
// Render stats
let combat_stats = ecs.read_storage::<CombatStats>();
let pools = ecs.read_storage::<Pools>();
let attributes = ecs.read_storage::<Attributes>();
let players = ecs.read_storage::<Player>();
let hunger = ecs.read_storage::<HungerClock>();
for (_player, stats, hunger) in (&players, &combat_stats, &hunger).join() {
draw_lerping_bar(ctx, 2, 53, 26, stats.hp, stats.max_hp, RGB::from_u8(0, 255, 0), RGB::from_u8(255, 0, 0));
//ctx.draw_bar_horizontal(2, 53, 26, stats.hp, stats.max_hp, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK));
draw_lerping_bar(ctx, 2, 54, 26, stats.hp, stats.max_hp, RGB::named(rltk::BLUE), RGB::named(rltk::BLACK));
let skills = ecs.read_storage::<Skills>();
for (_player, stats, attributes, hunger, skills) in (&players, &pools, &attributes, &hunger, &skills).join() {
// Draw hp/mana bars
draw_lerping_bar(
ctx,
2,
53,
26,
stats.hit_points.current,
stats.hit_points.max,
RGB::from_u8(0, 255, 0),
RGB::from_u8(255, 0, 0),
);
draw_lerping_bar(
ctx,
2,
54,
26,
stats.mana.current,
stats.mana.max,
RGB::named(rltk::BLUE),
RGB::named(rltk::BLACK),
);
// Draw AC
let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*skills);
let mut armour_ac_bonus = 0;
let equipped = ecs.read_storage::<Equipped>();
let ac = ecs.read_storage::<ArmourClassBonus>();
let player_entity = ecs.fetch::<Entity>();
for (wielded, ac) in (&equipped, &ac).join() {
if wielded.owner == *player_entity {
armour_ac_bonus += ac.amount;
}
}
let armour_class = stats.bac - attributes.dexterity.bonus - skill_ac_bonus - armour_ac_bonus;
ctx.print_color(30, 53, RGB::named(rltk::PINK), RGB::named(rltk::BLACK), "AC");
ctx.print_color(32, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), armour_class);
// Draw level
ctx.print_color(
30,
54,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
format!("XP{}/{}", stats.level, stats.xp),
);
// Draw attributes
ctx.print_color(36, 53, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "STR");
ctx.print_color(39, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.strength.base);
ctx.print_color(43, 53, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "DEX");
ctx.print_color(46, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.dexterity.base);
ctx.print_color(50, 53, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "CON");
ctx.print_color(53, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.constitution.base);
ctx.print_color(36, 54, RGB::named(rltk::CYAN), RGB::named(rltk::BLACK), "INT");
ctx.print_color(39, 54, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.intelligence.base);
ctx.print_color(43, 54, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "WIS");
ctx.print_color(46, 54, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.wisdom.base);
ctx.print_color(50, 54, RGB::named(rltk::PURPLE), RGB::named(rltk::BLACK), "CHA");
ctx.print_color(53, 54, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.charisma.base);
// Draw hunger
match hunger.state {
HungerState::Satiated => {
ctx.print_color_right(70, 53, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Satiated")

View file

@ -1,6 +1,6 @@
use super::{
gamelog, CombatStats, Confusion, Consumable, Cursed, Destructible, Digger, Equippable, Equipped, HungerClock,
HungerState, InBackpack, InflictsDamage, MagicMapper, Map, Name, ParticleBuilder, Point, Position, ProvidesHealing,
gamelog, Confusion, Consumable, Cursed, Destructible, Digger, Equippable, Equipped, HungerClock, HungerState,
InBackpack, InflictsDamage, MagicMapper, Map, Name, ParticleBuilder, Point, Pools, Position, ProvidesHealing,
ProvidesNutrition, RandomNumberGenerator, RunState, SufferDamage, TileType, Viewshed, Wand, WantsToDropItem,
WantsToPickupItem, WantsToRemoveItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME,
};
@ -60,7 +60,7 @@ impl<'a> System<'a> for ItemUseSystem {
ReadStorage<'a, ProvidesHealing>,
ReadStorage<'a, ProvidesNutrition>,
WriteStorage<'a, HungerClock>,
WriteStorage<'a, CombatStats>,
WriteStorage<'a, Pools>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>,
@ -268,7 +268,8 @@ impl<'a> System<'a> for ItemUseSystem {
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);
stats.hit_points.current =
i32::min(stats.hit_points.max, stats.hit_points.current + heal.amount);
if entity == *player_entity {
gamelog::Logger::new().append("Quaffing, you recover some vigour.").log();
}

View file

@ -35,6 +35,7 @@ mod inventory_system;
use inventory_system::*;
mod particle_system;
use particle_system::{ParticleBuilder, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME};
mod gamesystem;
mod random_table;
mod rex_assets;
@ -212,10 +213,10 @@ impl State {
.append("recover some of your strength")
.period()
.log();
let mut player_health_store = self.ecs.write_storage::<CombatStats>();
let player_health = player_health_store.get_mut(*player_entity);
if let Some(player_health) = player_health {
player_health.hp = i32::max(player_health.hp, player_health.max_hp / 2);
let mut pools = self.ecs.write_storage::<Pools>();
let stats = pools.get_mut(*player_entity);
if let Some(stats) = stats {
stats.hit_points.current = i32::max(stats.hit_points.current, stats.hit_points.max / 2);
}
}
@ -524,16 +525,18 @@ fn main() -> rltk::BError {
gs.ecs.register::<BlocksTile>();
gs.ecs.register::<BlocksVisibility>();
gs.ecs.register::<Door>();
gs.ecs.register::<CombatStats>();
gs.ecs.register::<Pools>();
gs.ecs.register::<Attributes>();
gs.ecs.register::<Skills>();
gs.ecs.register::<HungerClock>();
gs.ecs.register::<WantsToMelee>();
gs.ecs.register::<SufferDamage>();
gs.ecs.register::<Item>();
gs.ecs.register::<Equippable>();
gs.ecs.register::<Equipped>();
gs.ecs.register::<MeleePowerBonus>();
gs.ecs.register::<DefenceBonus>();
gs.ecs.register::<MeleeWeapon>();
gs.ecs.register::<NaturalAttacks>();
gs.ecs.register::<ArmourClassBonus>();
gs.ecs.register::<Cursed>();
gs.ecs.register::<ProvidesHealing>();
gs.ecs.register::<InflictsDamage>();
@ -562,11 +565,11 @@ fn main() -> rltk::BError {
raws::load_raws();
let player_entity = spawner::player(&mut gs.ecs, 0, 0);
gs.ecs.insert(rltk::RandomNumberGenerator::new());
gs.ecs.insert(Map::new(1, 64, 64, 0));
gs.ecs.insert(Point::new(0, 0));
let player_entity = spawner::player(&mut gs.ecs, 0, 0);
gs.ecs.insert(player_entity);
gs.ecs.insert(rltk::RandomNumberGenerator::new());
gs.ecs.insert(RunState::MapGeneration {});
gs.ecs.insert(particle_system::ParticleBuilder::new());
gs.ecs.insert(rex_assets::RexAssets::new());

View file

@ -69,7 +69,7 @@ pub const GOBLINS_4X4: PrefabVault =
PrefabVault { template: GOBLINS_4X4_V, width: 4, height: 4, first_id: 0, last_id: 100, can_flip: Flipping::Both };
const GOBLINS_4X4_V: &str = "
#^  
#G#
#$#
#g# 
^g^
";
@ -78,13 +78,13 @@ pub const GOBLINS2_4X4: PrefabVault =
PrefabVault { template: GOBLINS2_4X4_V, width: 4, height: 4, first_id: 0, last_id: 100, can_flip: Flipping::Both };
const GOBLINS2_4X4_V: &str = "
#^#g
G# #
$# #
g# 
# g^
";
pub const GOBLINS_5X5: PrefabVault =
PrefabVault { template: GOBLINS_5X5_V, width: 5, height: 5, first_id: 0, last_id: 100, can_flip: Flipping::Both };
PrefabVault { template: GOBLINS_5X5_V, width: 5, height: 5, first_id: 3, last_id: 100, can_flip: Flipping::Both };
const GOBLINS_5X5_V: &str = "
 ^#g 
G#?#^
@ -94,7 +94,7 @@ G#?#^
";
pub const GOBLINS_6X6: PrefabVault =
PrefabVault { template: GOBLINS_6X6_V, width: 6, height: 6, first_id: 0, last_id: 100, can_flip: Flipping::Both };
PrefabVault { template: GOBLINS_6X6_V, width: 6, height: 6, first_id: 5, last_id: 100, can_flip: Flipping::Both };
const GOBLINS_6X6_V: &str = "
   #  
 #^#g 
@ -157,7 +157,7 @@ const HOUSE_TRAP_7X7_V: &str = "
";
pub const ORC_HOUSE_8X8: PrefabVault =
PrefabVault { template: ORC_HOUSE_8X8_V, width: 8, height: 8, first_id: 0, last_id: 100, can_flip: Flipping::Both };
PrefabVault { template: ORC_HOUSE_8X8_V, width: 8, height: 8, first_id: 2, last_id: 100, can_flip: Flipping::Both };
const ORC_HOUSE_8X8_V: &str = "
######

View file

@ -233,14 +233,8 @@ impl TownBuilder {
build_data: &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator,
) {
for y in building.1..building.1 + building.3 {
for x in building.0..building.0 + building.2 {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] == TileType::WoodFloor && idx != 0 && rng.roll_dice(1, 3) == 1 {
build_data.spawn_list.push((idx, "rat".to_string()));
}
}
}
let mut to_place: Vec<&str> = vec!["rat", "rat", "rat"];
self.random_building_spawn(building, build_data, rng, &mut to_place, 0);
}
fn grass_layer(&mut self, build_data: &mut BuilderMap) {

View file

@ -1,6 +1,6 @@
use super::{
gamelog, CombatStats, DefenceBonus, Equipped, HungerClock, HungerState, MeleePowerBonus, Name, ParticleBuilder,
Position, SufferDamage, WantsToMelee,
gamelog, gamesystem, ArmourClassBonus, Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon,
Name, NaturalAttacks, ParticleBuilder, Pools, Position, Skill, Skills, SufferDamage, WantsToMelee, WeaponAttribute,
};
use specs::prelude::*;
@ -12,14 +12,18 @@ impl<'a> System<'a> for MeleeCombatSystem {
ReadExpect<'a, Entity>,
WriteStorage<'a, WantsToMelee>,
ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>,
ReadStorage<'a, Attributes>,
ReadStorage<'a, Skills>,
ReadStorage<'a, Pools>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>,
ReadStorage<'a, Equipped>,
ReadStorage<'a, DefenceBonus>,
ReadStorage<'a, MeleePowerBonus>,
ReadStorage<'a, MeleeWeapon>,
ReadStorage<'a, NaturalAttacks>,
ReadStorage<'a, ArmourClassBonus>,
ReadStorage<'a, HungerClock>,
WriteExpect<'a, rltk::RandomNumberGenerator>,
);
fn run(&mut self, data: Self::SystemData) {
@ -28,58 +32,146 @@ impl<'a> System<'a> for MeleeCombatSystem {
player_entity,
mut wants_melee,
names,
combat_stats,
attributes,
skills,
pools,
mut inflict_damage,
mut particle_builder,
positions,
equipped,
defence_bonuses,
melee_power_bonuses,
melee_weapons,
natural_attacks,
ac,
hunger_clock,
mut rng,
) = data;
for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
if stats.hp <= 0 {
for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in
(&entities, &wants_melee, &names, &attributes, &skills, &pools).join()
{
let target_pools = pools.get(wants_melee.target).unwrap();
let target_attributes = attributes.get(wants_melee.target).unwrap();
let target_skills = skills.get(wants_melee.target).unwrap();
if attacker_pools.hit_points.current <= 0 {
break;
}
let target_stats = combat_stats.get(wants_melee.target).unwrap();
if target_stats.hp <= 0 {
if target_pools.hit_points.current <= 0 {
break;
}
let target_name = names.get(wants_melee.target).unwrap();
let mut offensive_bonus = 0;
for (_item_entity, power_bonus, equipped_by) in (&entities, &melee_power_bonuses, &equipped).join() {
if equipped_by.owner == entity {
offensive_bonus += power_bonus.amount;
let mut weapon_info = MeleeWeapon {
attribute: WeaponAttribute::Strength,
hit_bonus: 0,
damage_n_dice: 1,
damage_die_type: 4,
damage_bonus: 0,
};
let mut attack_verb = "hits";
if let Some(nat) = natural_attacks.get(entity) {
rltk::console::log("Natural attack found");
if !nat.attacks.is_empty() {
let attack_index = if nat.attacks.len() == 1 {
0
} else {
rng.roll_dice(1, nat.attacks.len() as i32) as usize - 1
};
weapon_info.hit_bonus = nat.attacks[attack_index].hit_bonus;
weapon_info.damage_n_dice = nat.attacks[attack_index].damage_n_dice;
weapon_info.damage_die_type = nat.attacks[attack_index].damage_die_type;
weapon_info.damage_bonus = nat.attacks[attack_index].damage_bonus;
attack_verb = &nat.attacks[attack_index].name;
}
}
let mut defensive_bonus = 0;
for (_item_entity, defence_bonus, equipped_by) in (&entities, &defence_bonuses, &equipped).join() {
if equipped_by.owner == wants_melee.target {
defensive_bonus += defence_bonus.amount;
for (wielded, melee) in (&equipped, &melee_weapons).join() {
if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee {
weapon_info = melee.clone();
}
}
// Get all offensive bonuses
let d20 = rng.roll_dice(1, 20);
let attribute_hit_bonus = attacker_attributes.strength.bonus;
let skill_hit_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_hit_bonus = weapon_info.hit_bonus;
let mut status_hit_bonus = 0;
let hc = hunger_clock.get(entity);
if let Some(hc) = hc {
match hc.state {
HungerState::Satiated => {
offensive_bonus += 1;
status_hit_bonus += 1;
}
HungerState::Weak => {
offensive_bonus -= 1;
status_hit_bonus -= 1;
}
HungerState::Fainting => {
offensive_bonus -= 1;
defensive_bonus -= 1;
status_hit_bonus -= 2;
}
_ => {}
}
}
let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defence + defensive_bonus));
let modified_hit_roll = d20 - attribute_hit_bonus - skill_hit_bonus - weapon_hit_bonus - status_hit_bonus;
if damage == 0 {
// Get armour class
let bac = target_pools.bac;
let attribute_ac_bonus = target_attributes.dexterity.bonus;
let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*target_skills);
let mut armour_ac_bonus = 0;
for (wielded, ac) in (&equipped, &ac).join() {
if wielded.owner == wants_melee.target {
armour_ac_bonus += ac.amount;
}
}
let armour_class = bac - attribute_ac_bonus - skill_ac_bonus - armour_ac_bonus;
let target_number = 10 + armour_class + attacker_pools.level;
if d20 != 1 && (d20 == 20 || modified_hit_roll < target_number) {
// Target hit!
let base_damage = rng.roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type);
let attribute_damage_bonus = attacker_attributes.strength.bonus;
let skill_damage_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_damage_bonus = weapon_info.damage_bonus;
let damage =
i32::max(0, base_damage + attribute_damage_bonus + skill_damage_bonus + weapon_damage_bonus);
let pos = positions.get(wants_melee.target);
if let Some(pos) = pos {
particle_builder.damage_taken(pos.x, pos.y)
}
SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage);
if entity == *player_entity {
gamelog::Logger::new() // You hit the <name>.
.append("You hit the")
.npc_name_n(&target_name.name)
.period()
.log();
} else if wants_melee.target == *player_entity {
gamelog::Logger::new() // <name> hits you!
.append("The")
.npc_name(&name.name)
.append(attack_verb)
.append("you!")
.log();
} else {
gamelog::Logger::new() // <name> misses the <target>.
.append("The")
.npc_name(&name.name)
.append(attack_verb)
.append("the")
.npc_name_n(&target_name.name)
.period()
.log();
}
} else {
let pos = positions.get(wants_melee.target);
if let Some(pos) = pos {
particle_builder.attack_miss(pos.x, pos.y)
}
if entity == *player_entity {
gamelog::Logger::new() // You miss.
.append("You miss.")
@ -101,35 +193,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
.period()
.log();
}
} else {
if entity == *player_entity {
gamelog::Logger::new() // You hit the <name>.
.append("You hit the")
.npc_name_n(&target_name.name)
.period()
.log();
} else if wants_melee.target == *player_entity {
gamelog::Logger::new() // <name> hits you!
.append("The")
.npc_name(&name.name)
.colour(rltk::WHITE)
.append("hits you!")
.log();
} else {
gamelog::Logger::new() // <name> misses the <target>.
.append("The")
.npc_name(&name.name)
.colour(rltk::WHITE)
.append("hits the")
.npc_name_n(&target_name.name)
.period()
.log();
}
let pos = positions.get(wants_melee.target);
if let Some(pos) = pos {
particle_builder.damage_taken(pos.x, pos.y)
}
SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage);
}
}

View file

@ -67,6 +67,17 @@ impl ParticleBuilder {
);
}
pub fn attack_miss(&mut self, x: i32, y: i32) {
self.request(
x,
y,
rltk::RGB::named(rltk::CYAN),
rltk::RGB::named(rltk::BLACK),
rltk::to_cp437('‼'),
DEFAULT_PARTICLE_LIFETIME,
);
}
pub fn trap_triggered(&mut self, x: i32, y: i32) {
self.request(
x,

View file

@ -1,7 +1,7 @@
use super::{
gamelog, BlocksTile, BlocksVisibility, Bystander, CombatStats, Door, EntityMoved, Hidden, HungerClock, HungerState,
Item, Map, Monster, Name, ParticleBuilder, Player, Position, Renderable, RunState, State, SufferDamage, Telepath,
TileType, Viewshed, WantsToMelee, WantsToPickupItem,
gamelog, Attributes, BlocksTile, BlocksVisibility, Bystander, Door, EntityMoved, Hidden, HungerClock, HungerState,
Item, Map, Monster, Name, ParticleBuilder, Player, Pools, Position, Renderable, RunState, State, SufferDamage,
Telepath, TileType, Viewshed, WantsToMelee, WantsToPickupItem,
};
use rltk::{Point, RandomNumberGenerator, Rltk, VirtualKeyCode};
use specs::prelude::*;
@ -11,6 +11,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let mut viewsheds = ecs.write_storage::<Viewshed>();
let attributes = ecs.read_storage::<Attributes>();
let map = ecs.fetch::<Map>();
let entities = ecs.entities();
@ -24,7 +25,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
let mut result = RunState::AwaitingInput;
let mut door_pos: Option<Point> = None;
for (_entity, _player, pos) in (&entities, &mut players, &mut positions).join() {
for (_entity, _player, pos, attributes) in (&entities, &mut players, &mut positions, &attributes).join() {
let delta_x = i;
let delta_y = j;
@ -46,7 +47,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
if let Some(name) = names.get(*potential_target) {
gamelog::Logger::new().append("The").item_name(&name.name).append("is blocked.").log();
}
} else if rng.roll_dice(1, 6) == 1 {
} else if rng.roll_dice(1, 6) + attributes.strength.bonus < 5 {
if let Some(name) = names.get(*potential_target) {
gamelog::Logger::new().append("The").item_name(&name.name).append("resists!").log();
}
@ -91,6 +92,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState {
let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let mut viewsheds = ecs.write_storage::<Viewshed>();
let attributes = ecs.read_storage::<Attributes>();
let map = ecs.fetch::<Map>();
let entities = ecs.entities();
@ -104,7 +106,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState {
let mut result = RunState::AwaitingInput;
let mut door_pos: Option<Point> = None;
for (_entity, _player, pos) in (&entities, &mut players, &mut positions).join() {
for (_entity, _player, pos, attributes) in (&entities, &mut players, &mut positions, &attributes).join() {
let delta_x = i;
let delta_y = j;
@ -122,7 +124,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState {
let door = doors.get_mut(*potential_target);
if let Some(door) = door {
if door.open == false {
if rng.roll_dice(1, 6) == 1 {
if rng.roll_dice(1, 6) + attributes.strength.bonus < 5 {
if let Some(name) = names.get(*potential_target) {
gamelog::Logger::new().append("The").item_name(&name.name).append("resists!").log();
}
@ -166,6 +168,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState {
let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let mut viewsheds = ecs.write_storage::<Viewshed>();
let attributes = ecs.read_storage::<Attributes>();
let map = ecs.fetch::<Map>();
let entities = ecs.entities();
@ -173,7 +176,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState {
let names = ecs.read_storage::<Name>();
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
for (entity, _player, pos) in (&entities, &mut players, &mut positions).join() {
for (entity, _player, pos, attributes) in (&entities, &mut players, &mut positions, &attributes).join() {
let delta_x = i;
let delta_y = j;
@ -210,8 +213,8 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState {
if door.open == false {
let mut particle_builder = ecs.write_resource::<ParticleBuilder>();
particle_builder.kick(pos.x + delta_x, pos.y + delta_y);
// 33% chance of breaking it down.
if rng.roll_dice(1, 3) == 1 {
// 33% chance of breaking it down + str
if rng.roll_dice(1, 6) + attributes.strength.bonus > 4 {
gamelog::Logger::new()
.append("As you kick the")
.item_name_n(target_name)
@ -273,7 +276,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool {
let mut telepaths = ecs.write_storage::<Telepath>();
let mut entity_moved = ecs.write_storage::<EntityMoved>();
let friendlies = ecs.read_storage::<Bystander>();
let combat_stats = ecs.read_storage::<CombatStats>();
let pools = ecs.read_storage::<Pools>();
let map = ecs.fetch::<Map>();
let entities = ecs.entities();
@ -304,7 +307,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool {
ppos.x = pos.x;
ppos.y = pos.y;
} else {
let target = combat_stats.get(*potential_target);
let target = pools.get(*potential_target);
if let Some(_target) = target {
wants_to_melee
.insert(entity, WantsToMelee { target: *potential_target })
@ -538,12 +541,12 @@ fn skip_turn(ecs: &mut World) -> bool {
let mut did_heal = false;
if can_heal {
let mut health_components = ecs.write_storage::<CombatStats>();
let player_hp = health_components.get_mut(*player_entity).unwrap();
let mut health_components = ecs.write_storage::<Pools>();
let pools = health_components.get_mut(*player_entity).unwrap();
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let roll = rng.roll_dice(1, 6);
if (roll == 6) && player_hp.hp < player_hp.max_hp {
player_hp.hp += 1;
if (roll == 6) && pools.hit_points.current < pools.hit_points.max {
pools.hit_points.current += 1;
did_heal = true;
}
}

View file

@ -1,5 +1,6 @@
use super::Renderable;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
pub struct Mob {
@ -7,16 +8,28 @@ pub struct Mob {
pub name: String,
pub renderable: Option<Renderable>,
pub flags: Option<Vec<String>>,
pub stats: MobStats,
pub level: Option<i32>,
pub bac: Option<i32>,
pub attacks: Option<Vec<NaturalAttack>>,
pub attributes: Option<MobAttributes>,
pub skills: Option<HashMap<String, i32>>,
pub vision_range: i32,
pub ai: String,
pub quips: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]
pub struct MobStats {
pub max_hp: i32,
pub hp: i32,
pub power: i32,
pub defence: i32,
pub struct MobAttributes {
pub str: Option<i32>,
pub dex: Option<i32>,
pub con: Option<i32>,
pub int: Option<i32>,
pub wis: Option<i32>,
pub cha: Option<i32>,
}
#[derive(Deserialize, Debug)]
pub struct NaturalAttack {
pub name: String,
pub hit_bonus: i32,
pub damage: String,
}

View file

@ -1,11 +1,16 @@
use super::Raws;
use crate::components::*;
use crate::gamesystem::*;
use crate::random_table::RandomTable;
use regex::Regex;
use specs::prelude::*;
use specs::saveload::{MarkedBuilder, SimpleMarker};
use std::collections::{HashMap, HashSet};
pub enum SpawnType {
AtPosition { x: i32, y: i32 },
Equipped { by: Entity },
Carried { by: Entity },
}
pub struct RawMaster {
@ -67,36 +72,32 @@ impl RawMaster {
}
}
pub fn spawn_named_entity(
raws: &RawMaster,
new_entity: EntityBuilder,
key: &str,
pos: SpawnType,
rng: &mut rltk::RandomNumberGenerator,
) -> Option<Entity> {
pub fn spawn_named_entity(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
if raws.item_index.contains_key(key) {
return spawn_named_item(raws, new_entity, key, pos);
return spawn_named_item(raws, ecs, key, pos);
} else if raws.mob_index.contains_key(key) {
return spawn_named_mob(raws, new_entity, key, pos, rng);
return spawn_named_mob(raws, ecs, key, pos);
} else if raws.prop_index.contains_key(key) {
return spawn_named_prop(raws, new_entity, key, pos);
return spawn_named_prop(raws, ecs, key, pos);
}
None
}
pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str, pos: SpawnType) -> Option<Entity> {
pub fn spawn_named_item(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
if raws.item_index.contains_key(key) {
let item_template = &raws.raws.items[raws.item_index[key]];
let mut eb = new_entity;
let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
eb = eb.with(Name { name: item_template.name.name.clone(), plural: item_template.name.plural.clone() });
eb = eb.with(Item {});
eb = spawn_position(pos, eb);
eb = spawn_position(pos, eb, key, raws);
if let Some(renderable) = &item_template.renderable {
eb = eb.with(get_renderable_component(renderable));
}
let mut weapon_type = -1;
if let Some(flags) = &item_template.flags {
for flag in flags.iter() {
match flag.as_str() {
@ -105,13 +106,25 @@ pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str,
"CURSED" => eb = eb.with(Cursed {}),
"EQUIP_MELEE" => eb = eb.with(Equippable { slot: EquipmentSlot::Melee }),
"EQUIP_SHIELD" => eb = eb.with(Equippable { slot: EquipmentSlot::Shield }),
"EQUIP_HEAD" => eb = eb.with(Equippable { slot: EquipmentSlot::Head }),
"EQUIP_TORSO" => eb = eb.with(Equippable { slot: EquipmentSlot::Torso }),
"EQUIP_LEGS" => eb = eb.with(Equippable { slot: EquipmentSlot::Legs }),
"EQUIP_FEET" => eb = eb.with(Equippable { slot: EquipmentSlot::Feet }),
"EQUIP_HANDS" => eb = eb.with(Equippable { slot: EquipmentSlot::Hands }),
"EQUIP_NECK" => eb = eb.with(Equippable { slot: EquipmentSlot::Neck }),
"WAND" => eb = eb.with(Wand { uses: 3, max_uses: 3 }),
"FOOD" => eb = eb.with(ProvidesNutrition {}),
"STRENGTH" => weapon_type = 0,
"DEXTERITY" => weapon_type = 2,
"FINESSE" => weapon_type = 3,
_ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())),
}
}
}
let mut base_damage = "1d4";
let mut hit_bonus = 0;
if let Some(effects_list) = &item_template.effects {
for effect in effects_list.iter() {
let effect_name = effect.0.as_str();
@ -121,8 +134,9 @@ pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str,
"damage" => eb = eb.with(InflictsDamage { amount: effect.1.parse::<i32>().unwrap() }),
"aoe" => eb = eb.with(AOE { radius: effect.1.parse::<i32>().unwrap() }),
"confusion" => eb = eb.with(Confusion { turns: effect.1.parse::<i32>().unwrap() }),
"melee_power_bonus" => eb = eb.with(MeleePowerBonus { amount: effect.1.parse::<i32>().unwrap() }),
"defence_bonus" => eb = eb.with(DefenceBonus { amount: effect.1.parse::<i32>().unwrap() }),
"base_damage" => base_damage = effect.1,
"hit_bonus" => hit_bonus = effect.1.parse::<i32>().unwrap(),
"ac" => eb = eb.with(ArmourClassBonus { amount: effect.1.parse::<i32>().unwrap() }),
"magicmapper" => eb = eb.with(MagicMapper {}),
"digger" => eb = eb.with(Digger {}),
_ => rltk::console::log(format!("Warning: effect {} not implemented.", effect_name)),
@ -130,77 +144,158 @@ pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str,
}
}
if weapon_type != -1 {
let (n_dice, die_type, bonus) = parse_dice_string(base_damage);
let mut wpn = MeleeWeapon {
attribute: WeaponAttribute::Strength,
damage_n_dice: n_dice,
damage_die_type: die_type,
damage_bonus: bonus,
hit_bonus: hit_bonus,
};
match weapon_type {
0 => wpn.attribute = WeaponAttribute::Strength,
1 => wpn.attribute = WeaponAttribute::Dexterity,
_ => wpn.attribute = WeaponAttribute::Finesse,
}
eb = eb.with(wpn);
}
return Some(eb.build());
}
None
}
pub fn spawn_named_mob(
raws: &RawMaster,
new_entity: EntityBuilder,
key: &str,
pos: SpawnType,
rng: &mut rltk::RandomNumberGenerator,
) -> Option<Entity> {
pub fn spawn_named_mob(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
if raws.mob_index.contains_key(key) {
let mob_template = &raws.raws.mobs[raws.mob_index[key]];
let mut eb;
// New entity with a position, name, combatstats, and viewshed
let mut eb = new_entity;
eb = spawn_position(pos, eb);
eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() });
match mob_template.ai.as_ref() {
"bystander" => eb = eb.with(Bystander {}),
"melee" => eb = eb.with(Monster {}),
_ => {}
}
let rolled_hp = roll_hit_dice(rng, 1, mob_template.stats.max_hp);
eb = eb.with(CombatStats {
max_hp: rolled_hp,
hp: rolled_hp,
power: mob_template.stats.power,
defence: mob_template.stats.defence,
});
eb = eb.with(Viewshed { visible_tiles: Vec::new(), range: mob_template.vision_range, dirty: true });
eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
eb = spawn_position(pos, eb, key, raws);
eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() });
eb = eb.with(Viewshed { visible_tiles: Vec::new(), range: mob_template.vision_range, dirty: true });
if let Some(renderable) = &mob_template.renderable {
eb = eb.with(get_renderable_component(renderable));
}
if let Some(flags) = &mob_template.flags {
for flag in flags.iter() {
match flag.as_str() {
"BLOCKS_TILE" => eb = eb.with(BlocksTile {}),
"BYSTANDER" => eb = eb.with(Bystander {}),
"MONSTER" => eb = eb.with(Monster {}),
_ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())),
}
}
}
if let Some(quips) = &mob_template.quips {
eb = eb.with(Quips { available: quips.clone() });
}
// Setup combat stats
let mut attr = Attributes {
strength: Attribute { base: 10, modifiers: 0, bonus: 0 },
dexterity: Attribute { base: 10, modifiers: 0, bonus: 0 },
constitution: Attribute { base: 10, modifiers: 0, bonus: 0 },
intelligence: Attribute { base: 10, modifiers: 0, bonus: 0 },
wisdom: Attribute { base: 10, modifiers: 0, bonus: 0 },
charisma: Attribute { base: 10, modifiers: 0, bonus: 0 },
};
let mut mob_con = 10;
let mut mob_int = 10;
if let Some(attributes) = &mob_template.attributes {
if let Some(str) = attributes.str {
attr.strength = Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) };
}
if let Some(dex) = attributes.dex {
attr.strength = Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) };
}
if let Some(con) = attributes.con {
attr.constitution = Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) };
mob_con = con;
}
if let Some(int) = attributes.int {
attr.intelligence = Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) };
mob_int = int;
}
if let Some(wis) = attributes.wis {
attr.wisdom = Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) };
}
if let Some(cha) = attributes.cha {
attr.charisma = Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) };
}
}
eb = eb.with(attr);
let mob_level = if mob_template.level.is_some() { mob_template.level.unwrap() } else { 1 };
// Should really use existing RNG here
let mut rng = rltk::RandomNumberGenerator::new();
let mob_hp = npc_hp(&mut rng, mob_con, mob_level);
let mob_mana = mana_at_level(&mut rng, mob_int, mob_level);
let mob_bac = if mob_template.bac.is_some() { mob_template.bac.unwrap() } else { 10 };
let pools = Pools {
level: mob_level,
xp: 0,
bac: mob_bac,
hit_points: Pool { current: mob_hp, max: mob_hp },
mana: Pool { current: mob_mana, max: mob_mana },
};
eb = eb.with(pools);
let mut skills = Skills { skills: HashMap::new() };
skills.skills.insert(Skill::Melee, 0);
skills.skills.insert(Skill::Defence, 0);
if let Some(mobskills) = &mob_template.skills {
for sk in mobskills.iter() {
match sk.0.as_str() {
"melee" => {
skills.skills.insert(Skill::Melee, *sk.1);
}
"defence" => {
skills.skills.insert(Skill::Defence, *sk.1);
}
"magic" => {
skills.skills.insert(Skill::Magic, *sk.1);
}
_ => {
rltk::console::log(format!("Unknown skill referenced: [{}]", sk.0));
}
}
}
}
eb = eb.with(skills);
if let Some(natural_attacks) = &mob_template.attacks {
let mut natural = NaturalAttacks { attacks: Vec::new() };
for na in natural_attacks.iter() {
let (n, d, b) = parse_dice_string(&na.damage);
let attack = NaturalAttack {
name: na.name.clone(),
hit_bonus: na.hit_bonus,
damage_n_dice: n,
damage_die_type: d,
damage_bonus: b,
};
natural.attacks.push(attack);
}
eb = eb.with(natural);
}
return Some(eb.build());
}
None
}
pub fn roll_hit_dice(rng: &mut rltk::RandomNumberGenerator, n: i32, d: i32) -> i32 {
let mut rolled_hp: i32 = 0;
for _i in 0..n {
rolled_hp += rng.roll_dice(1, d);
}
return rolled_hp;
}
pub fn spawn_named_prop(raws: &RawMaster, new_entity: EntityBuilder, key: &str, pos: SpawnType) -> Option<Entity> {
pub fn spawn_named_prop(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
if raws.prop_index.contains_key(key) {
let prop_template = &raws.raws.props[raws.prop_index[key]];
let mut eb = new_entity;
eb = spawn_position(pos, eb);
let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
eb = spawn_position(pos, eb, key, raws);
if let Some(renderable) = &prop_template.renderable {
eb = eb.with(get_renderable_component(renderable));
}
@ -237,12 +332,15 @@ pub fn spawn_named_prop(raws: &RawMaster, new_entity: EntityBuilder, key: &str,
None
}
fn spawn_position(pos: SpawnType, new_entity: EntityBuilder) -> EntityBuilder {
fn spawn_position<'a>(pos: SpawnType, new_entity: EntityBuilder<'a>, tag: &str, raws: &RawMaster) -> EntityBuilder<'a> {
let mut eb = new_entity;
match pos {
SpawnType::AtPosition { x, y } => {
eb = eb.with(Position { x, y });
SpawnType::AtPosition { x, y } => eb = eb.with(Position { x, y }),
SpawnType::Carried { by } => eb = eb.with(InBackpack { owner: by }),
SpawnType::Equipped { by } => {
let slot = find_slot_for_equippable_item(tag, raws);
eb = eb.with(Equipped { owner: by, slot })
}
}
@ -283,3 +381,42 @@ pub fn table_by_name(raws: &RawMaster, key: &str, difficulty: i32) -> RandomTabl
return RandomTable::new().add("debug", 1);
}
}
pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) {
lazy_static! {
static ref DICE_RE: Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap();
}
let mut n_dice = 1;
let mut die_type = 4;
let mut die_bonus = 0;
for cap in DICE_RE.captures_iter(dice) {
if let Some(group) = cap.get(1) {
n_dice = group.as_str().parse::<i32>().expect("Not a digit");
}
if let Some(group) = cap.get(2) {
die_type = group.as_str().parse::<i32>().expect("Not a digit");
}
if let Some(group) = cap.get(3) {
die_bonus = group.as_str().parse::<i32>().expect("Not a digit");
}
}
(n_dice, die_type, die_bonus)
}
fn find_slot_for_equippable_item(tag: &str, raws: &RawMaster) -> EquipmentSlot {
if !raws.item_index.contains_key(tag) {
panic!("Trying to equip an unknown item: {}", tag);
}
let item_index = raws.item_index[tag];
let item = &raws.raws.items[item_index];
if let Some(flags) = &item.flags {
for flag in flags {
match flag.as_str() {
"EQUIP_MELEE" => return EquipmentSlot::Melee,
"EQUIP_SHIELD" => return EquipmentSlot::Shield,
_ => {}
}
}
}
panic!("Trying to equip {}, but it has no slot tag.", tag);
}

View file

@ -48,15 +48,14 @@ pub fn save_game(ecs: &mut World) {
serializer,
data,
AOE,
ArmourClassBonus,
Attributes,
BlocksTile,
BlocksVisibility,
Bystander,
CombatStats,
Confusion,
Consumable,
Cursed,
DefenceBonus,
Destructible,
Digger,
Door,
@ -70,13 +69,15 @@ pub fn save_game(ecs: &mut World) {
InflictsDamage,
Item,
MagicMapper,
MeleePowerBonus,
MeleeWeapon,
Mind,
Monster,
NaturalAttacks,
Name,
ParticleLifetime,
Player,
Position,
Pools,
Prop,
ProvidesHealing,
ProvidesNutrition,
@ -84,6 +85,7 @@ pub fn save_game(ecs: &mut World) {
Ranged,
Renderable,
SingleActivation,
Skills,
SufferDamage,
Telepath,
Viewshed,
@ -147,15 +149,14 @@ pub fn load_game(ecs: &mut World) {
de,
d,
AOE,
ArmourClassBonus,
Attributes,
BlocksTile,
BlocksVisibility,
Bystander,
CombatStats,
Confusion,
Consumable,
Cursed,
DefenceBonus,
Destructible,
Digger,
Door,
@ -169,12 +170,14 @@ pub fn load_game(ecs: &mut World) {
InflictsDamage,
Item,
MagicMapper,
MeleePowerBonus,
MeleeWeapon,
Mind,
Monster,
NaturalAttacks,
Name,
ParticleLifetime,
Player,
Pools,
Position,
Prop,
ProvidesHealing,
@ -183,6 +186,7 @@ pub fn load_game(ecs: &mut World) {
Ranged,
Renderable,
SingleActivation,
Skills,
SufferDamage,
Telepath,
Viewshed,

View file

@ -1,6 +1,7 @@
use super::{
random_table::RandomTable, raws, Attribute, Attributes, CombatStats, HungerClock, HungerState, Map, Name, Player,
Position, Rect, Renderable, SerializeMe, TileType, Viewshed,
gamesystem, gamesystem::attr_bonus, random_table::RandomTable, raws, Attribute, Attributes, HungerClock,
HungerState, Map, Name, Player, Pool, Pools, Position, Rect, Renderable, SerializeMe, Skill, Skills, TileType,
Viewshed,
};
use rltk::{RandomNumberGenerator, RGB};
use specs::prelude::*;
@ -9,8 +10,23 @@ use std::collections::HashMap;
/// Spawns the player and returns his/her entity object.
pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
let mut skills = Skills { skills: HashMap::new() };
skills.skills.insert(Skill::Melee, 0);
skills.skills.insert(Skill::Defence, 0);
skills.skills.insert(Skill::Magic, 0);
let mut rng = ecs.write_resource::<rltk::RandomNumberGenerator>();
let str = gamesystem::roll_4d6(&mut rng);
let dex = gamesystem::roll_4d6(&mut rng);
let con = gamesystem::roll_4d6(&mut rng);
let int = gamesystem::roll_4d6(&mut rng);
let wis = gamesystem::roll_4d6(&mut rng);
let cha = gamesystem::roll_4d6(&mut rng);
std::mem::drop(rng);
// d8 hit die - but always maxxed at level 1, so player doesn't have to roll.
ecs.create_entity()
let player = ecs
.create_entity()
.with(Position { x: player_x, y: player_y })
.with(Renderable {
glyph: rltk::to_cp437('@'),
@ -21,22 +37,39 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
.with(Player {})
.with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true })
.with(Name { name: "you".to_string(), plural: "you".to_string() })
.with(CombatStats { max_hp: 12, hp: 12, defence: 0, power: 4 })
.with(HungerClock { state: HungerState::Satiated, duration: 50 })
.with(Attributes {
strength: Attribute { base: 10, modifiers: 0, bonus: 0 },
dexterity: Attribute { base: 10, modifiers: 0, bonus: 0 },
constitution: Attribute { base: 10, modifiers: 0, bonus: 0 },
intelligence: Attribute { base: 10, modifiers: 0, bonus: 0 },
wisdom: Attribute { base: 10, modifiers: 0, bonus: 0 },
charisma: Attribute { base: 10, modifiers: 0, bonus: 0 },
strength: Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) },
dexterity: Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) },
constitution: Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) },
intelligence: Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) },
wisdom: Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) },
charisma: Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) },
})
.with(Pools {
hit_points: Pool { current: 10 + attr_bonus(con), max: 10 + attr_bonus(con) },
mana: Pool { current: 2 + attr_bonus(int), max: 2 + attr_bonus(int) },
xp: 0,
level: 1,
bac: 10,
})
.with(skills)
.marked::<SimpleMarker<SerializeMe>>()
.build()
.build();
raws::spawn_named_entity(
&raws::RAWS.lock().unwrap(),
ecs,
"equip_dagger",
raws::SpawnType::Equipped { by: player },
);
raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, "food_apple", raws::SpawnType::Carried { by: player });
return player;
}
// Consts
const MAX_ENTITIES: i32 = 3;
const MAX_ENTITIES: i32 = 2;
/// Fills a room with stuff!
pub fn spawn_room(map: &Map, rng: &mut RandomNumberGenerator, room: &Rect, spawn_list: &mut Vec<(usize, String)>) {
@ -61,6 +94,18 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize],
let mut areas: Vec<usize> = Vec::from(area);
let difficulty = map.difficulty;
if areas.len() == 0 {
rltk::console::log("DEBUGINFO: No areas capable of spawning mobs!");
return;
}
if rng.roll_dice(1, 3) == 1 {
let array_idx = if areas.len() == 1 { 0usize } else { (rng.roll_dice(1, areas.len() as i32) - 1) as usize };
let map_idx = areas[array_idx];
spawn_points.insert(map_idx, mob_table(difficulty).roll(rng));
areas.remove(array_idx);
}
let num_spawns = i32::min(areas.len() as i32, rng.roll_dice(1, MAX_ENTITIES + 2) - 2);
if num_spawns <= 0 {
return;
@ -70,7 +115,6 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize],
let category = category_table().roll(rng);
let spawn_table;
match category.as_ref() {
"mob" => spawn_table = mob_table(difficulty),
"item" => {
let item_category = item_category_table().roll(rng);
match item_category.as_ref() {
@ -104,13 +148,8 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) {
let y = (*spawn.0 / width) as i32;
std::mem::drop(map);
let spawn_result = raws::spawn_named_entity(
&raws::RAWS.lock().unwrap(),
ecs.create_entity(),
&spawn.1,
raws::SpawnType::AtPosition { x, y },
&mut rltk::RandomNumberGenerator::new(),
);
let spawn_result =
raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, &spawn.1, raws::SpawnType::AtPosition { x, y });
if spawn_result.is_some() {
return;
}
@ -118,9 +157,9 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) {
rltk::console::log(format!("WARNING: We don't know how to spawn [{}]!", spawn.1));
}
// 12 mobs : 6 items : 2 food : 1 trap
// 3 items : 1 food : 1 trap
fn category_table() -> RandomTable {
return RandomTable::new().add("mob", 12).add("item", 6).add("food", 2).add("trap", 1);
return RandomTable::new().add("item", 3).add("food", 1).add("trap", 1);
}
// 3 scrolls : 3 potions : 1 equipment : 1 wand?