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)", "bracket-lib 0.8.7 (git+https://github.com/amethyst/bracket-lib.git?rev=851f6f08675444fb6fa088b9e67bee9fd75554c6)",
"criterion", "criterion",
"lazy_static", "lazy_static",
"regex",
"rltk", "rltk",
"serde", "serde",
"serde_json", "serde_json",

View file

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

View file

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

View file

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

View file

@ -60,10 +60,13 @@
{ "id": "orc_large", "weight": 1, "difficulty": 3}, { "id": "orc_large", "weight": 1, "difficulty": 3},
{ "id": "goblin_chieftain", "weight": 1, "difficulty": 3}, { "id": "goblin_chieftain", "weight": 1, "difficulty": 3},
{ "id": "dog", "weight": 1, "difficulty": 4},
{ "id": "ogre", "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)] #[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct ProvidesNutrition {} pub struct ProvidesNutrition {}
#[derive(Component, Debug, ConvertSaveload, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CombatStats { pub struct Pool {
pub max_hp: i32, pub max: i32,
pub hp: i32, pub current: i32,
pub defence: i32, }
pub power: 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -115,6 +122,18 @@ pub struct Attribute {
pub bonus: i32, 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)] #[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Attributes { pub struct Attributes {
pub strength: Attribute, pub strength: Attribute,
@ -153,15 +172,46 @@ pub struct Item {}
pub enum EquipmentSlot { pub enum EquipmentSlot {
Melee, Melee,
Shield, 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)] #[derive(Component, ConvertSaveload, Clone)]
pub struct MeleePowerBonus { pub struct ArmourClassBonus {
pub amount: i32,
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct DefenceBonus {
pub amount: i32, 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::*; use specs::prelude::*;
pub struct DamageSystem {} pub struct DamageSystem {}
impl<'a> System<'a> for DamageSystem { impl<'a> System<'a> for DamageSystem {
type SystemData = ( type SystemData = (
WriteStorage<'a, CombatStats>, WriteStorage<'a, Pools>,
WriteStorage<'a, SufferDamage>, WriteStorage<'a, SufferDamage>,
WriteExpect<'a, Map>, WriteExpect<'a, Map>,
ReadStorage<'a, Position>, ReadStorage<'a, Position>,
@ -16,7 +16,7 @@ impl<'a> System<'a> for DamageSystem {
let (mut stats, mut damage, mut map, positions, entities) = data; let (mut stats, mut damage, mut map, positions, entities) = data;
for (entity, mut stats, damage) in (&entities, &mut stats, &damage).join() { 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); let pos = positions.get(entity);
if let Some(pos) = pos { if let Some(pos) = pos {
let idx = map.xy_idx(pos.x, pos.y); 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(); let mut dead: Vec<Entity> = Vec::new();
// Using scope to make borrow checker happy // 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 players = ecs.read_storage::<Player>();
let names = ecs.read_storage::<Name>(); let names = ecs.read_storage::<Name>();
let items = ecs.read_storage::<Item>(); let items = ecs.read_storage::<Item>();
let entities = ecs.entities(); let entities = ecs.entities();
for (entity, stats) in (&entities, &combat_stats).join() { for (entity, stats) in (&entities, &combat_stats).join() {
if stats.hp < 1 { if stats.hit_points.current < 1 {
let player = players.get(entity); let player = players.get(entity);
match player { match player {
None => { 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::{ use super::{
camera, gamelog, rex_assets::RexAssets, CombatStats, Equipped, Hidden, HungerClock, HungerState, InBackpack, Map, camera, gamelog, gamesystem, rex_assets::RexAssets, ArmourClassBonus, Attributes, Equipped, Hidden, HungerClock,
Name, Player, Point, Position, RunState, State, Viewshed, HungerState, InBackpack, Map, Name, Player, Point, Pools, Position, RunState, Skill, Skills, State, Viewshed,
}; };
use rltk::{Rltk, VirtualKeyCode, RGB}; use rltk::{Rltk, VirtualKeyCode, RGB};
use specs::prelude::*; 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 ctx.draw_hollow_box(71, 0, 28, 55, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)); // Side box
// Render stats // 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 players = ecs.read_storage::<Player>();
let hunger = ecs.read_storage::<HungerClock>(); let hunger = ecs.read_storage::<HungerClock>();
for (_player, stats, hunger) in (&players, &combat_stats, &hunger).join() { let skills = ecs.read_storage::<Skills>();
draw_lerping_bar(ctx, 2, 53, 26, stats.hp, stats.max_hp, RGB::from_u8(0, 255, 0), RGB::from_u8(255, 0, 0)); for (_player, stats, attributes, hunger, skills) in (&players, &pools, &attributes, &hunger, &skills).join() {
//ctx.draw_bar_horizontal(2, 53, 26, stats.hp, stats.max_hp, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK)); // Draw hp/mana bars
draw_lerping_bar(ctx, 2, 54, 26, stats.hp, stats.max_hp, RGB::named(rltk::BLUE), RGB::named(rltk::BLACK)); 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 { match hunger.state {
HungerState::Satiated => { HungerState::Satiated => {
ctx.print_color_right(70, 53, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Satiated") ctx.print_color_right(70, 53, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Satiated")

View file

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

View file

@ -35,6 +35,7 @@ mod inventory_system;
use inventory_system::*; use inventory_system::*;
mod particle_system; mod particle_system;
use particle_system::{ParticleBuilder, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME}; use particle_system::{ParticleBuilder, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME};
mod gamesystem;
mod random_table; mod random_table;
mod rex_assets; mod rex_assets;
@ -212,10 +213,10 @@ impl State {
.append("recover some of your strength") .append("recover some of your strength")
.period() .period()
.log(); .log();
let mut player_health_store = self.ecs.write_storage::<CombatStats>(); let mut pools = self.ecs.write_storage::<Pools>();
let player_health = player_health_store.get_mut(*player_entity); let stats = pools.get_mut(*player_entity);
if let Some(player_health) = player_health { if let Some(stats) = stats {
player_health.hp = i32::max(player_health.hp, player_health.max_hp / 2); 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::<BlocksTile>();
gs.ecs.register::<BlocksVisibility>(); gs.ecs.register::<BlocksVisibility>();
gs.ecs.register::<Door>(); gs.ecs.register::<Door>();
gs.ecs.register::<CombatStats>(); gs.ecs.register::<Pools>();
gs.ecs.register::<Attributes>(); gs.ecs.register::<Attributes>();
gs.ecs.register::<Skills>();
gs.ecs.register::<HungerClock>(); gs.ecs.register::<HungerClock>();
gs.ecs.register::<WantsToMelee>(); gs.ecs.register::<WantsToMelee>();
gs.ecs.register::<SufferDamage>(); gs.ecs.register::<SufferDamage>();
gs.ecs.register::<Item>(); gs.ecs.register::<Item>();
gs.ecs.register::<Equippable>(); gs.ecs.register::<Equippable>();
gs.ecs.register::<Equipped>(); gs.ecs.register::<Equipped>();
gs.ecs.register::<MeleePowerBonus>(); gs.ecs.register::<MeleeWeapon>();
gs.ecs.register::<DefenceBonus>(); gs.ecs.register::<NaturalAttacks>();
gs.ecs.register::<ArmourClassBonus>();
gs.ecs.register::<Cursed>(); gs.ecs.register::<Cursed>();
gs.ecs.register::<ProvidesHealing>(); gs.ecs.register::<ProvidesHealing>();
gs.ecs.register::<InflictsDamage>(); gs.ecs.register::<InflictsDamage>();
@ -562,11 +565,11 @@ fn main() -> rltk::BError {
raws::load_raws(); 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(Map::new(1, 64, 64, 0));
gs.ecs.insert(Point::new(0, 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(player_entity);
gs.ecs.insert(rltk::RandomNumberGenerator::new());
gs.ecs.insert(RunState::MapGeneration {}); gs.ecs.insert(RunState::MapGeneration {});
gs.ecs.insert(particle_system::ParticleBuilder::new()); gs.ecs.insert(particle_system::ParticleBuilder::new());
gs.ecs.insert(rex_assets::RexAssets::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 }; PrefabVault { template: GOBLINS_4X4_V, width: 4, height: 4, first_id: 0, last_id: 100, can_flip: Flipping::Both };
const GOBLINS_4X4_V: &str = " const GOBLINS_4X4_V: &str = "
#^   #^  
#G# #$#
#g#  #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 }; PrefabVault { template: GOBLINS2_4X4_V, width: 4, height: 4, first_id: 0, last_id: 100, can_flip: Flipping::Both };
const GOBLINS2_4X4_V: &str = " const GOBLINS2_4X4_V: &str = "
#^#g #^#g
G# # $# #
g#  g# 
# g^ # g^
"; ";
pub const GOBLINS_5X5: PrefabVault = 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 = " const GOBLINS_5X5_V: &str = "
 ^#g   ^#g 
G#?#^ G#?#^
@ -94,7 +94,7 @@ G#?#^
"; ";
pub const GOBLINS_6X6: PrefabVault = 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 = " const GOBLINS_6X6_V: &str = "
   #      #  
 #^#g   #^#g 
@ -157,7 +157,7 @@ const HOUSE_TRAP_7X7_V: &str = "
"; ";
pub const ORC_HOUSE_8X8: PrefabVault = 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 = " const ORC_HOUSE_8X8_V: &str = "
###### ######

View file

@ -233,14 +233,8 @@ impl TownBuilder {
build_data: &mut BuilderMap, build_data: &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator, rng: &mut rltk::RandomNumberGenerator,
) { ) {
for y in building.1..building.1 + building.3 { let mut to_place: Vec<&str> = vec!["rat", "rat", "rat"];
for x in building.0..building.0 + building.2 { self.random_building_spawn(building, build_data, rng, &mut to_place, 0);
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()));
}
}
}
} }
fn grass_layer(&mut self, build_data: &mut BuilderMap) { fn grass_layer(&mut self, build_data: &mut BuilderMap) {

View file

@ -1,6 +1,6 @@
use super::{ use super::{
gamelog, CombatStats, DefenceBonus, Equipped, HungerClock, HungerState, MeleePowerBonus, Name, ParticleBuilder, gamelog, gamesystem, ArmourClassBonus, Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon,
Position, SufferDamage, WantsToMelee, Name, NaturalAttacks, ParticleBuilder, Pools, Position, Skill, Skills, SufferDamage, WantsToMelee, WeaponAttribute,
}; };
use specs::prelude::*; use specs::prelude::*;
@ -12,14 +12,18 @@ impl<'a> System<'a> for MeleeCombatSystem {
ReadExpect<'a, Entity>, ReadExpect<'a, Entity>,
WriteStorage<'a, WantsToMelee>, WriteStorage<'a, WantsToMelee>,
ReadStorage<'a, Name>, ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>, ReadStorage<'a, Attributes>,
ReadStorage<'a, Skills>,
ReadStorage<'a, Pools>,
WriteStorage<'a, SufferDamage>, WriteStorage<'a, SufferDamage>,
WriteExpect<'a, ParticleBuilder>, WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>, ReadStorage<'a, Position>,
ReadStorage<'a, Equipped>, ReadStorage<'a, Equipped>,
ReadStorage<'a, DefenceBonus>, ReadStorage<'a, MeleeWeapon>,
ReadStorage<'a, MeleePowerBonus>, ReadStorage<'a, NaturalAttacks>,
ReadStorage<'a, ArmourClassBonus>,
ReadStorage<'a, HungerClock>, ReadStorage<'a, HungerClock>,
WriteExpect<'a, rltk::RandomNumberGenerator>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
@ -28,58 +32,146 @@ impl<'a> System<'a> for MeleeCombatSystem {
player_entity, player_entity,
mut wants_melee, mut wants_melee,
names, names,
combat_stats, attributes,
skills,
pools,
mut inflict_damage, mut inflict_damage,
mut particle_builder, mut particle_builder,
positions, positions,
equipped, equipped,
defence_bonuses, melee_weapons,
melee_power_bonuses, natural_attacks,
ac,
hunger_clock, hunger_clock,
mut rng,
) = data; ) = data;
for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() { for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in
if stats.hp <= 0 { (&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; break;
} }
let target_stats = combat_stats.get(wants_melee.target).unwrap(); if target_pools.hit_points.current <= 0 {
if target_stats.hp <= 0 {
break; break;
} }
let target_name = names.get(wants_melee.target).unwrap(); let target_name = names.get(wants_melee.target).unwrap();
let mut offensive_bonus = 0; let mut weapon_info = MeleeWeapon {
for (_item_entity, power_bonus, equipped_by) in (&entities, &melee_power_bonuses, &equipped).join() { attribute: WeaponAttribute::Strength,
if equipped_by.owner == entity { hit_bonus: 0,
offensive_bonus += power_bonus.amount; 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() { for (wielded, melee) in (&equipped, &melee_weapons).join() {
if equipped_by.owner == wants_melee.target { if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee {
defensive_bonus += defence_bonus.amount; 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); let hc = hunger_clock.get(entity);
if let Some(hc) = hc { if let Some(hc) = hc {
match hc.state { match hc.state {
HungerState::Satiated => { HungerState::Satiated => {
offensive_bonus += 1; status_hit_bonus += 1;
} }
HungerState::Weak => { HungerState::Weak => {
offensive_bonus -= 1; status_hit_bonus -= 1;
} }
HungerState::Fainting => { HungerState::Fainting => {
offensive_bonus -= 1; status_hit_bonus -= 2;
defensive_bonus -= 1;
} }
_ => {} _ => {}
} }
} }
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 { if entity == *player_entity {
gamelog::Logger::new() // You miss. gamelog::Logger::new() // You miss.
.append("You miss.") .append("You miss.")
@ -101,35 +193,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
.period() .period()
.log(); .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) { pub fn trap_triggered(&mut self, x: i32, y: i32) {
self.request( self.request(
x, x,

View file

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

View file

@ -1,5 +1,6 @@
use super::Renderable; use super::Renderable;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Mob { pub struct Mob {
@ -7,16 +8,28 @@ pub struct Mob {
pub name: String, pub name: String,
pub renderable: Option<Renderable>, pub renderable: Option<Renderable>,
pub flags: Option<Vec<String>>, 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 vision_range: i32,
pub ai: String,
pub quips: Option<Vec<String>>, pub quips: Option<Vec<String>>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct MobStats { pub struct MobAttributes {
pub max_hp: i32, pub str: Option<i32>,
pub hp: i32, pub dex: Option<i32>,
pub power: i32, pub con: Option<i32>,
pub defence: 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 super::Raws;
use crate::components::*; use crate::components::*;
use crate::gamesystem::*;
use crate::random_table::RandomTable; use crate::random_table::RandomTable;
use regex::Regex;
use specs::prelude::*; use specs::prelude::*;
use specs::saveload::{MarkedBuilder, SimpleMarker};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
pub enum SpawnType { pub enum SpawnType {
AtPosition { x: i32, y: i32 }, AtPosition { x: i32, y: i32 },
Equipped { by: Entity },
Carried { by: Entity },
} }
pub struct RawMaster { pub struct RawMaster {
@ -67,36 +72,32 @@ impl RawMaster {
} }
} }
pub fn spawn_named_entity( pub fn spawn_named_entity(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
raws: &RawMaster,
new_entity: EntityBuilder,
key: &str,
pos: SpawnType,
rng: &mut rltk::RandomNumberGenerator,
) -> Option<Entity> {
if raws.item_index.contains_key(key) { 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) { } 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) { } 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 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) { if raws.item_index.contains_key(key) {
let item_template = &raws.raws.items[raws.item_index[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(Name { name: item_template.name.name.clone(), plural: item_template.name.plural.clone() });
eb = eb.with(Item {}); eb = eb.with(Item {});
eb = spawn_position(pos, eb); eb = spawn_position(pos, eb, key, raws);
if let Some(renderable) = &item_template.renderable { if let Some(renderable) = &item_template.renderable {
eb = eb.with(get_renderable_component(renderable)); eb = eb.with(get_renderable_component(renderable));
} }
let mut weapon_type = -1;
if let Some(flags) = &item_template.flags { if let Some(flags) = &item_template.flags {
for flag in flags.iter() { for flag in flags.iter() {
match flag.as_str() { 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 {}), "CURSED" => eb = eb.with(Cursed {}),
"EQUIP_MELEE" => eb = eb.with(Equippable { slot: EquipmentSlot::Melee }), "EQUIP_MELEE" => eb = eb.with(Equippable { slot: EquipmentSlot::Melee }),
"EQUIP_SHIELD" => eb = eb.with(Equippable { slot: EquipmentSlot::Shield }), "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 }), "WAND" => eb = eb.with(Wand { uses: 3, max_uses: 3 }),
"FOOD" => eb = eb.with(ProvidesNutrition {}), "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())), _ => 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 { if let Some(effects_list) = &item_template.effects {
for effect in effects_list.iter() { for effect in effects_list.iter() {
let effect_name = effect.0.as_str(); 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() }), "damage" => eb = eb.with(InflictsDamage { amount: effect.1.parse::<i32>().unwrap() }),
"aoe" => eb = eb.with(AOE { radius: 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() }), "confusion" => eb = eb.with(Confusion { turns: effect.1.parse::<i32>().unwrap() }),
"melee_power_bonus" => eb = eb.with(MeleePowerBonus { amount: effect.1.parse::<i32>().unwrap() }), "base_damage" => base_damage = effect.1,
"defence_bonus" => eb = eb.with(DefenceBonus { amount: effect.1.parse::<i32>().unwrap() }), "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 {}), "magicmapper" => eb = eb.with(MagicMapper {}),
"digger" => eb = eb.with(Digger {}), "digger" => eb = eb.with(Digger {}),
_ => rltk::console::log(format!("Warning: effect {} not implemented.", effect_name)), _ => 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()); return Some(eb.build());
} }
None None
} }
pub fn spawn_named_mob( pub fn spawn_named_mob(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
raws: &RawMaster,
new_entity: EntityBuilder,
key: &str,
pos: SpawnType,
rng: &mut rltk::RandomNumberGenerator,
) -> Option<Entity> {
if raws.mob_index.contains_key(key) { if raws.mob_index.contains_key(key) {
let mob_template = &raws.raws.mobs[raws.mob_index[key]]; let mob_template = &raws.raws.mobs[raws.mob_index[key]];
let mut eb;
// New entity with a position, name, combatstats, and viewshed // New entity with a position, name, combatstats, and viewshed
let mut eb = new_entity; eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
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 = 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 { if let Some(renderable) = &mob_template.renderable {
eb = eb.with(get_renderable_component(renderable)); eb = eb.with(get_renderable_component(renderable));
} }
if let Some(flags) = &mob_template.flags { if let Some(flags) = &mob_template.flags {
for flag in flags.iter() { for flag in flags.iter() {
match flag.as_str() { match flag.as_str() {
"BLOCKS_TILE" => eb = eb.with(BlocksTile {}), "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())), _ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())),
} }
} }
} }
if let Some(quips) = &mob_template.quips { if let Some(quips) = &mob_template.quips {
eb = eb.with(Quips { available: quips.clone() }); 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()); return Some(eb.build());
} }
None None
} }
pub fn roll_hit_dice(rng: &mut rltk::RandomNumberGenerator, n: i32, d: i32) -> i32 { pub fn spawn_named_prop(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
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> {
if raws.prop_index.contains_key(key) { if raws.prop_index.contains_key(key) {
let prop_template = &raws.raws.props[raws.prop_index[key]]; let prop_template = &raws.raws.props[raws.prop_index[key]];
let mut eb = new_entity; let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
eb = spawn_position(pos, eb); eb = spawn_position(pos, eb, key, raws);
if let Some(renderable) = &prop_template.renderable { if let Some(renderable) = &prop_template.renderable {
eb = eb.with(get_renderable_component(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 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; let mut eb = new_entity;
match pos { match pos {
SpawnType::AtPosition { x, y } => { SpawnType::AtPosition { x, y } => eb = eb.with(Position { 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); 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, serializer,
data, data,
AOE, AOE,
ArmourClassBonus,
Attributes, Attributes,
BlocksTile, BlocksTile,
BlocksVisibility, BlocksVisibility,
Bystander, Bystander,
CombatStats,
Confusion, Confusion,
Consumable, Consumable,
Cursed, Cursed,
DefenceBonus,
Destructible, Destructible,
Digger, Digger,
Door, Door,
@ -70,13 +69,15 @@ pub fn save_game(ecs: &mut World) {
InflictsDamage, InflictsDamage,
Item, Item,
MagicMapper, MagicMapper,
MeleePowerBonus, MeleeWeapon,
Mind, Mind,
Monster, Monster,
NaturalAttacks,
Name, Name,
ParticleLifetime, ParticleLifetime,
Player, Player,
Position, Position,
Pools,
Prop, Prop,
ProvidesHealing, ProvidesHealing,
ProvidesNutrition, ProvidesNutrition,
@ -84,6 +85,7 @@ pub fn save_game(ecs: &mut World) {
Ranged, Ranged,
Renderable, Renderable,
SingleActivation, SingleActivation,
Skills,
SufferDamage, SufferDamage,
Telepath, Telepath,
Viewshed, Viewshed,
@ -147,15 +149,14 @@ pub fn load_game(ecs: &mut World) {
de, de,
d, d,
AOE, AOE,
ArmourClassBonus,
Attributes, Attributes,
BlocksTile, BlocksTile,
BlocksVisibility, BlocksVisibility,
Bystander, Bystander,
CombatStats,
Confusion, Confusion,
Consumable, Consumable,
Cursed, Cursed,
DefenceBonus,
Destructible, Destructible,
Digger, Digger,
Door, Door,
@ -169,12 +170,14 @@ pub fn load_game(ecs: &mut World) {
InflictsDamage, InflictsDamage,
Item, Item,
MagicMapper, MagicMapper,
MeleePowerBonus, MeleeWeapon,
Mind, Mind,
Monster, Monster,
NaturalAttacks,
Name, Name,
ParticleLifetime, ParticleLifetime,
Player, Player,
Pools,
Position, Position,
Prop, Prop,
ProvidesHealing, ProvidesHealing,
@ -183,6 +186,7 @@ pub fn load_game(ecs: &mut World) {
Ranged, Ranged,
Renderable, Renderable,
SingleActivation, SingleActivation,
Skills,
SufferDamage, SufferDamage,
Telepath, Telepath,
Viewshed, Viewshed,

View file

@ -1,6 +1,7 @@
use super::{ use super::{
random_table::RandomTable, raws, Attribute, Attributes, CombatStats, HungerClock, HungerState, Map, Name, Player, gamesystem, gamesystem::attr_bonus, random_table::RandomTable, raws, Attribute, Attributes, HungerClock,
Position, Rect, Renderable, SerializeMe, TileType, Viewshed, HungerState, Map, Name, Player, Pool, Pools, Position, Rect, Renderable, SerializeMe, Skill, Skills, TileType,
Viewshed,
}; };
use rltk::{RandomNumberGenerator, RGB}; use rltk::{RandomNumberGenerator, RGB};
use specs::prelude::*; use specs::prelude::*;
@ -9,8 +10,23 @@ use std::collections::HashMap;
/// Spawns the player and returns his/her entity object. /// Spawns the player and returns his/her entity object.
pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { 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. // 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(Position { x: player_x, y: player_y })
.with(Renderable { .with(Renderable {
glyph: rltk::to_cp437('@'), glyph: rltk::to_cp437('@'),
@ -21,22 +37,39 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
.with(Player {}) .with(Player {})
.with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true }) .with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true })
.with(Name { name: "you".to_string(), plural: "you".to_string() }) .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(HungerClock { state: HungerState::Satiated, duration: 50 })
.with(Attributes { .with(Attributes {
strength: Attribute { base: 10, modifiers: 0, bonus: 0 }, strength: Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) },
dexterity: Attribute { base: 10, modifiers: 0, bonus: 0 }, dexterity: Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) },
constitution: Attribute { base: 10, modifiers: 0, bonus: 0 }, constitution: Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) },
intelligence: Attribute { base: 10, modifiers: 0, bonus: 0 }, intelligence: Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) },
wisdom: Attribute { base: 10, modifiers: 0, bonus: 0 }, wisdom: Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) },
charisma: Attribute { base: 10, modifiers: 0, bonus: 0 }, 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>>() .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 // Consts
const MAX_ENTITIES: i32 = 3; const MAX_ENTITIES: i32 = 2;
/// Fills a room with stuff! /// 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)>) {
@ -61,6 +94,18 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize],
let mut areas: Vec<usize> = Vec::from(area); let mut areas: Vec<usize> = Vec::from(area);
let difficulty = map.difficulty; 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); let num_spawns = i32::min(areas.len() as i32, rng.roll_dice(1, MAX_ENTITIES + 2) - 2);
if num_spawns <= 0 { if num_spawns <= 0 {
return; return;
@ -70,7 +115,6 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize],
let category = category_table().roll(rng); let category = category_table().roll(rng);
let spawn_table; let spawn_table;
match category.as_ref() { match category.as_ref() {
"mob" => spawn_table = mob_table(difficulty),
"item" => { "item" => {
let item_category = item_category_table().roll(rng); let item_category = item_category_table().roll(rng);
match item_category.as_ref() { 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; let y = (*spawn.0 / width) as i32;
std::mem::drop(map); std::mem::drop(map);
let spawn_result = raws::spawn_named_entity( let spawn_result =
&raws::RAWS.lock().unwrap(), raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, &spawn.1, raws::SpawnType::AtPosition { x, y });
ecs.create_entity(),
&spawn.1,
raws::SpawnType::AtPosition { x, y },
&mut rltk::RandomNumberGenerator::new(),
);
if spawn_result.is_some() { if spawn_result.is_some() {
return; 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)); 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 { 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? // 3 scrolls : 3 potions : 1 equipment : 1 wand?