Add clothing to player and NPCs

This commit is contained in:
Timothy Warren 2022-01-04 11:11:38 -05:00
parent d96d1ce003
commit 5b227115db
9 changed files with 459 additions and 72 deletions

View File

@ -184,6 +184,49 @@
}
}
},
{
"name": "Dried Sausage",
"renderable": {
"glyph": "%",
"fg": "#00FF00",
"bg": "#000000",
"order": 2
},
"consumable": {
"effects": {
"food": ""
}
}
},
{
"name": "Beer",
"renderable": {
"glyph": "!",
"fg": "#FF00FF",
"bg": "#000000",
"order": 2
},
"consumable": {
"effects": {
"provides_healing": "4"
}
}
},
{
"name": "Rusty Longsword",
"renderable": {
"glyph": "/",
"fg": "#BB77BB",
"bg": "#000000",
"order": 2
},
"weapon": {
"range": "melee",
"attribute": "Might",
"base_damage": "1d8-1",
"hit_bonus": -1
}
},
{
"name": "Dagger",
"renderable": {
@ -194,7 +237,9 @@
},
"weapon": {
"range": "melee",
"power_bonus": 2
"attribute": "Quickness",
"base_damage": "1d4",
"hit_bonus": 0
}
},
{
@ -207,7 +252,9 @@
},
"weapon": {
"range": "melee",
"power_bonus": 4
"attribute": "Might",
"base_damage": "1d8",
"hit_bonus": 0
}
},
{
@ -220,7 +267,9 @@
},
"weapon": {
"range": "melee",
"power_bonus": 5
"attribute": "Might",
"base_damage": "1d8+1",
"hit_bonus": 0
}
},
{
@ -231,8 +280,9 @@
"bg": "#000000",
"order": 2
},
"shield": {
"defense_bonus": 1
"wearable": {
"slot": "Shield",
"armor_class": 1.0
}
},
{
@ -243,8 +293,102 @@
"bg": "#000000",
"order": 2
},
"shield": {
"defense_bonus": 3
"wearable": {
"slot": "Shield",
"armor_class": 2.0
}
},
{
"name": "Stained Tunic",
"renderable": {
"glyph": "[",
"fg": "#00FF00",
"bg": "#000000",
"order": 2
},
"wearable": {
"slot": "Torso",
"armor_class": 0.1
}
},
{
"name": "Torn Trousers",
"renderable": {
"glyph": "[",
"fg": "#00FFFF",
"bg": "#000000",
"order": 2
},
"wearable": {
"slot": "Legs",
"armor_class": 0.1
}
},
{
"name": "Old Boots",
"renderable": {
"glyph": "[",
"fg": "#FF9999",
"bg": "#000000",
"order": 2
},
"wearable": {
"slot": "Legs",
"armor_class": 0.1
}
},
{
"name": "Cudgel",
"renderable": {
"glyph": "/",
"fg": "#A52A2A",
"bg": "#000000",
"order": 2
},
"weapon": {
"range": "melee",
"attribute": "Quickness",
"base_damage": "1d4",
"hit_bonus": 0
}
},
{
"name": "Cloth Tunic",
"renderable": {
"glyph": "[",
"fg": "#00FF00",
"bg": "#000000",
"order": 2
},
"wearable": {
"slot": "Torso",
"armor_class": 0.1
}
},
{
"name": "Cloth Pants",
"renderable": {
"glyph": "[",
"fg": "#00FFFF",
"bg": "#000000",
"order": 2
},
"wearable": {
"slot": "Legs",
"armor_class": 0.1
}
},
{
"name": "Slippers",
"renderable": {
"glyph": "[",
"fg": "#FF9999",
"bg": "#000000",
"order": 2
},
"wearable": {
"slot": "Legs",
"armor_class": 0.1
}
}
],
@ -265,7 +409,13 @@
},
"skills": {
"Melee": 2
}
},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Shady Salesman",
@ -278,7 +428,13 @@
"blocks_tile": true,
"vision_range": 4,
"ai": "vendor",
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Patron",
@ -296,7 +452,13 @@
"Oh my, I drank too much.",
"Still saving the world, eh?"
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Priest",
@ -309,7 +471,13 @@
"blocks_tile": true,
"vision_range": 4,
"ai": "bystander",
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Parishioner",
@ -327,7 +495,13 @@
"I hear there's going to be a good sermon on tea",
"Want some cake?"
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Blacksmith",
@ -340,7 +514,13 @@
"blocks_tile": true,
"vision_range": 4,
"ai": "vendor",
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Clothier",
@ -353,7 +533,13 @@
"blocks_tile": true,
"vision_range": 4,
"ai": "vendor",
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Alchemist",
@ -366,7 +552,13 @@
"blocks_tile": true,
"vision_range": 4,
"ai": "vendor",
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Mom",
@ -385,7 +577,13 @@
"Be careful in the dungeon!",
"Your father would be so proud, were he here."
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Peasant",
@ -401,7 +599,13 @@
"quips": [
"Why are you in my house?"
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Dock Worker",
@ -419,7 +623,13 @@
"Nice weather",
"Hello"
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Fisher",
@ -437,7 +647,13 @@
"I caught something, but it wasn't a fish!",
"Looks like rain"
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Wannabe Pirate",
@ -455,7 +671,13 @@
"Grog!",
"Booze!"
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Drunk",
@ -473,7 +695,13 @@
"Need... more... booze!",
"Spare a copper?"
],
"attributes": {}
"attributes": {},
"equipped": [
"Cudgel",
"Cloth Tunic",
"Cloth Pants",
"Slippers"
]
},
{
"name": "Rat",
@ -493,6 +721,16 @@
"skills": {
"Melee": -1,
"Defense": -1
},
"natural": {
"armor_class": 11,
"attacks": [
{
"name": "bite",
"hit_bonus": 0,
"damage": "1d4"
}
]
}
},
{

View File

@ -165,6 +165,11 @@ pub struct WantsToRemoveItem {
pub enum EquipmentSlot {
Melee,
Shield,
Head,
Torso,
Legs,
Feet,
Hands,
}
#[derive(Component, Serialize, Deserialize, Clone)]
@ -194,8 +199,9 @@ pub struct MeleeWeapon {
}
#[derive(Component, ConvertSaveload, Clone)]
pub struct DefenseBonus {
pub defense: i32,
pub struct Wearable {
pub armor_class: f32,
pub slot: EquipmentSlot,
}
#[derive(Component, Serialize, Deserialize, Clone)]

View File

@ -519,7 +519,7 @@ fn main() -> ::rltk::BError {
Equippable,
Equipped,
MeleeWeapon,
DefenseBonus,
Wearable,
WantsToRemoveItem,
ParticleLifetime,
HungerClock,

View File

@ -2,12 +2,13 @@ use ::rltk::{RandomNumberGenerator, RGB};
use ::specs::prelude::*;
use crate::components::{
Attributes, HungerClock, HungerState, Name, Pools, Skill, Skills, SufferDamage, WantsToMelee,
Attributes, Equipped, HungerClock, HungerState, MeleeWeapon, Name, Pools, Skill, Skills,
SufferDamage, WantsToMelee, Wearable,
};
use crate::game_log::GameLog;
use crate::gamesystem::skill_bonus;
use crate::particle_system::ParticleBuilder;
use crate::Position;
use crate::{EquipmentSlot, Position, WeaponAttribute};
pub struct MeleeCombatSystem {}
@ -26,6 +27,9 @@ impl<'a> System<'a> for MeleeCombatSystem {
ReadStorage<'a, HungerClock>,
ReadStorage<'a, Pools>,
WriteExpect<'a, RandomNumberGenerator>,
ReadStorage<'a, Equipped>,
ReadStorage<'a, MeleeWeapon>,
ReadStorage<'a, Wearable>,
);
fn run(&mut self, data: Self::SystemData) {
@ -42,6 +46,9 @@ impl<'a> System<'a> for MeleeCombatSystem {
hunger_clock,
pools,
mut rng,
equipped_items,
meleeweapons,
wearables,
) = data;
for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in (
@ -61,10 +68,28 @@ impl<'a> System<'a> for MeleeCombatSystem {
if attacker_pools.hit_points.current > 0 && target_pools.hit_points.current > 0 {
let target_name = names.get(wants_melee.target).unwrap();
let mut weapon_info = MeleeWeapon {
attribute: WeaponAttribute::Might,
hit_bonus: 0,
damage_n_dice: 1,
damage_die_type: 4,
damage_bonus: 0,
};
for (wielded, melee) in (&equipped_items, &meleeweapons).join() {
if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee {
weapon_info = melee.clone();
}
}
let natural_roll = rng.roll_dice(1, 20);
let attribute_hit_bonus = attacker_attributes.might.bonus;
let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might {
attacker_attributes.might.bonus
} else {
attacker_attributes.quickness.bonus
};
let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_hit_bonus = 0; // TODO: Once weapons support this
let weapon_hit_bonus = weapon_info.hit_bonus;
let mut status_hit_bonus = 0;
if let Some(hc) = hunger_clock.get(entity) {
// Well-Fed grants +1
@ -78,10 +103,21 @@ impl<'a> System<'a> for MeleeCombatSystem {
+ weapon_hit_bonus
+ status_hit_bonus;
let mut armor_item_bonus_f = 0.0;
for (wielded, armor) in (&equipped_items, &wearables).join() {
if wielded.owner == wants_melee.target {
armor_item_bonus_f += armor.armor_class;
}
}
// let base_armor_class = match natural.get(wants_melee.target) {
// None => 10,
// Some(nat) = nat.armor_class.unwrap_or(10);
// };
let base_armor_class = 10;
let armor_quickness_bonus = target_attributes.quickness.bonus;
let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills);
let armor_item_bonus = 0; // TODO: Once armor supports this
let armor_item_bonus = armor_item_bonus_f as i32;
let armor_class =
base_armor_class + armor_quickness_bonus + armor_skill_bonus + armor_item_bonus;
@ -90,7 +126,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
let base_damage = rng.roll_dice(1, 4);
let attr_damage_bonus = attacker_attributes.might.bonus;
let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_damage_bonus = 0;
let weapon_damage_bonus = weapon_info.damage_bonus;
let damage = i32::max(
0,

View File

@ -8,7 +8,7 @@ pub struct Item {
pub renderable: Option<Renderable>,
pub consumable: Option<Consumable>,
pub weapon: Option<Weapon>,
pub shield: Option<Shield>,
pub wearable: Option<Wearable>,
}
#[derive(Deserialize, Debug)]
@ -33,6 +33,7 @@ pub struct Weapon {
}
#[derive(Deserialize, Debug)]
pub struct Shield {
pub defense_bonus: i32,
pub struct Wearable {
pub armor_class: f32,
pub slot: String,
}

View File

@ -17,6 +17,7 @@ pub struct Mob {
pub level: Option<i32>,
pub hp: Option<i32>,
pub mana: Option<i32>,
pub equipped: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]

View File

@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet};
use ::regex::Regex;
use ::specs::prelude::*;
use ::specs::saveload::{MarkedBuilder, SimpleMarker};
use crate::components::*;
use crate::gamesystem::{mana_at_level, npc_hp};
@ -16,7 +17,7 @@ pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) {
let mut die_type = 4;
let mut die_bonus = 0;
for cap in DIC_RE.captures_iter(dice) {
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");
}
@ -33,6 +34,8 @@ pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) {
pub enum SpawnType {
AtPosition { x: i32, y: i32 },
Equipped { by: Entity },
Carried { by: Entity },
}
pub struct RawMaster {
@ -103,17 +106,40 @@ impl RawMaster {
}
}
fn spawn_position(pos: SpawnType, new_entity: EntityBuilder) -> EntityBuilder {
let mut eb = new_entity;
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 item.weapon.is_some() {
return EquipmentSlot::Melee;
} else if let Some(wearable) = &item.wearable {
return string_to_slot(&wearable.slot);
}
panic!("Trying to equip {}, but it has not slot tag", tag);
}
fn spawn_position<'a>(
pos: SpawnType,
new_entity: EntityBuilder<'a>,
tag: &str,
raws: &RawMaster,
) -> EntityBuilder<'a> {
let eb = new_entity;
// Spawn in the specified location
match pos {
SpawnType::AtPosition { x, y } => {
eb = eb.with(Position { x, y });
SpawnType::AtPosition { x, y } => eb.with(Position { x, y }),
SpawnType::Carried { by } => eb.with(InBackpack { owner: by }),
SpawnType::Equipped { by } => {
let slot = find_slot_for_equippable_item(tag, raws);
eb.with(Equipped { owner: by, slot })
}
}
eb
}
fn get_renderable_component(
@ -127,19 +153,36 @@ fn get_renderable_component(
}
}
pub fn string_to_slot(slot: &str) -> EquipmentSlot {
match slot {
"Shield" => EquipmentSlot::Shield,
"Head" => EquipmentSlot::Head,
"Torso" => EquipmentSlot::Torso,
"Legs" => EquipmentSlot::Legs,
"Feet" => EquipmentSlot::Feet,
"Hands" => EquipmentSlot::Hands,
"Melee" => EquipmentSlot::Melee,
_ => {
rltk::console::log(format!("Warning: unknown equipment slot type [{}]", slot));
EquipmentSlot::Melee
}
}
}
pub fn spawn_named_item(
raws: &RawMaster,
new_entity: EntityBuilder,
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>>();
// Spawn in the specified location
eb = spawn_position(pos, eb);
eb = spawn_position(pos, eb, key, raws);
// Renderable
if let Some(renderable) = &item_template.renderable {
@ -196,17 +239,29 @@ pub fn spawn_named_item(
eb = eb.with(Equippable {
slot: EquipmentSlot::Melee,
});
eb = eb.with(MeleePowerBonus {
power: weapon.power_bonus,
});
let (n_dice, die_type, bonus) = parse_dice_string(&weapon.base_damage);
let mut wpn = MeleeWeapon {
attribute: WeaponAttribute::Might,
damage_n_dice: n_dice,
damage_die_type: die_type,
damage_bonus: bonus,
hit_bonus: weapon.hit_bonus,
};
wpn.attribute = match weapon.attribute.as_str() {
"Quickness" => WeaponAttribute::Quickness,
_ => WeaponAttribute::Might,
};
eb = eb.with(wpn);
}
if let Some(shield) = &item_template.shield {
eb = eb.with(Equippable {
slot: EquipmentSlot::Shield,
});
eb = eb.with(DefenseBonus {
defense: shield.defense_bonus,
if let Some(wearable) = &item_template.wearable {
let slot = string_to_slot(&wearable.slot);
eb = eb.with(Equippable { slot });
eb = eb.with(Wearable {
slot,
armor_class: wearable.armor_class,
});
}
@ -218,17 +273,17 @@ pub fn spawn_named_item(
pub fn spawn_named_mob(
raws: &RawMaster,
new_entity: EntityBuilder,
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;
let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
// Spawn in the specified location
eb = spawn_position(pos, eb);
eb = spawn_position(pos, eb, key, raws);
// Renderable
if let Some(renderable) = &mob_template.renderable {
@ -251,6 +306,10 @@ pub fn spawn_named_mob(
});
}
if mob_template.blocks_tile {
eb = eb.with(BlocksTile {});
}
let mut mob_fitness = 11;
let mut mob_int = 11;
let mut attr = Attributes {
@ -318,17 +377,22 @@ pub fn spawn_named_mob(
}
eb = eb.with(skills);
if mob_template.blocks_tile {
eb = eb.with(BlocksTile {});
}
eb = eb.with(Viewshed {
visible_tiles: Vec::new(),
range: mob_template.vision_range,
dirty: true,
});
return Some(eb.build());
let new_mob = eb.build();
// Are they weilding anything?
if let Some(wielding) = &mob_template.equipped {
for tag in wielding.iter() {
spawn_named_entity(raws, ecs, tag, SpawnType::Equipped { by: new_mob });
}
}
return Some(new_mob);
}
None
@ -336,17 +400,17 @@ pub fn spawn_named_mob(
pub fn spawn_named_prop(
raws: &RawMaster,
new_entity: EntityBuilder,
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;
let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
// Spawn in the specified location
eb = spawn_position(pos, eb);
eb = spawn_position(pos, eb, key, raws);
// Renderable
if let Some(renderable) = &prop_template.renderable {
@ -396,16 +460,16 @@ pub fn spawn_named_prop(
pub fn spawn_named_entity(
raws: &RawMaster,
new_entity: EntityBuilder,
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);
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

View File

@ -76,7 +76,7 @@ pub fn save_game(ecs: &mut World) {
Equippable,
Equipped,
MeleeWeapon,
DefenseBonus,
Wearable,
WantsToRemoveItem,
ParticleLifetime,
HungerClock,
@ -172,7 +172,7 @@ pub fn load_game(ecs: &mut World) {
Equippable,
Equipped,
MeleeWeapon,
DefenseBonus,
Wearable,
WantsToRemoveItem,
ParticleLifetime,
HungerClock,

View File

@ -12,7 +12,8 @@ use crate::{Map, Rect, TileType};
/// Spawns the player and returns their entity object
pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
ecs.create_entity()
let player = ecs
.create_entity()
.with(Position {
x: player_x,
y: player_y,
@ -50,7 +51,47 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
level: 1,
})
.marked::<SimpleMarker<SerializeMe>>()
.build()
.build();
// Starting equipment
spawn_named_entity(
&RAWS.lock().unwrap(),
ecs,
"Rusty Longsword",
SpawnType::Equipped { by: player },
);
spawn_named_entity(
&RAWS.lock().unwrap(),
ecs,
"Dried Sausage",
SpawnType::Carried { by: player },
);
spawn_named_entity(
&RAWS.lock().unwrap(),
ecs,
"Beer",
SpawnType::Carried { by: player },
);
spawn_named_entity(
&RAWS.lock().unwrap(),
ecs,
"Stained Tunic",
SpawnType::Equipped { by: player },
);
spawn_named_entity(
&RAWS.lock().unwrap(),
ecs,
"Torn Trousers",
SpawnType::Equipped { by: player },
);
spawn_named_entity(
&RAWS.lock().unwrap(),
ecs,
"Old Boots",
SpawnType::Equipped { by: player },
);
player
}
const MAX_MONSTERS: i32 = 4;
@ -136,7 +177,7 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) {
let item_result = spawn_named_entity(
&RAWS.lock().unwrap(),
ecs.create_entity(),
ecs,
spawn.1,
SpawnType::AtPosition { x, y },
);