fixed some attributes double-dipping, and added COMBAT_LOGGING

This commit is contained in:
Llywelwyn 2023-07-28 17:12:02 +01:00
parent c2759e9e8e
commit 6e6d364aa5
5 changed files with 119 additions and 19 deletions

View file

@ -97,7 +97,7 @@
"name": "giant rat",
"renderable": { "glyph": "r", "fg": "#bb8000", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 2,
"level": 1,
"bac": 7,
"vision_range": 8,
"attacks": [
@ -133,6 +133,7 @@
"name": "goblin",
"renderable": { "glyph": "g", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 1,
"vision_range": 12,
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d4"}
@ -143,6 +144,7 @@
"name": "kobold",
"renderable": { "glyph": "k", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 1,
"vision_range": 7,
"attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "1d4"}
@ -175,6 +177,7 @@
"name": "coyote",
"renderable": { "glyph": "d", "fg": "#6E3215", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 1,
"bac": 7,
"vision_range": 12,
"attacks": [
@ -186,6 +189,7 @@
"name": "wolf",
"renderable": { "glyph": "d", "fg": "#5E4225", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"],
"level": 5,
"bac": 4,
"vision_range": 12,
"attacks": [

View file

@ -17,6 +17,9 @@ pub fn player_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i
}
pub fn npc_hp(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 {
if level == 0 {
return rng.roll_dice(1, 4);
}
let mut total = 1;
for _i in 0..level {
total += rng.roll_dice(1, 8) + attr_bonus(constitution);

View file

@ -46,6 +46,21 @@ impl<'a> System<'a> for MeleeCombatSystem {
mut rng,
) = data;
// Combat works with the older system of AC being a bonus to to-hit to the attacker. When an
// attacker tries to hit a victim, the attacker rolls a d20, and must roll *less than* the
// value of 10 + victim's AC + attacker's to-hit bonuses.
//
// e.g. An attacker with +0 to-hit hitting a target with 10 AC:
// 1d20 must be less than 20, 95% chance of a hit.
//
// e.g. An attacker with +1 to-hit from being satiated hits a rat with 7 AC:
// 1d20 must be less than 18 (10+7+1), 85% chance of a hit.
//
// e.g. An attacker with +0 to-hit hitting a target with 0 AC:
// 1d20 must be less than 10, 45% chance of a hit
const COMBAT_LOGGING: bool = true;
for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in
(&entities, &wants_melee, &names, &attributes, &skills, &pools).join()
{
@ -72,7 +87,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
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
@ -95,7 +109,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
// Get all offensive bonuses
let d20 = rng.roll_dice(1, 20);
let attribute_hit_bonus = attacker_attributes.strength.bonus;
let attribute_hit_bonus = attacker_attributes.dexterity.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;
@ -114,7 +128,8 @@ impl<'a> System<'a> for MeleeCombatSystem {
_ => {}
}
}
let modified_hit_roll = d20 - attribute_hit_bonus - skill_hit_bonus - weapon_hit_bonus - status_hit_bonus;
let attacker_bonuses =
attacker_pools.level + attribute_hit_bonus + skill_hit_bonus + weapon_hit_bonus + status_hit_bonus;
// Get armour class
let bac = target_pools.bac;
@ -126,18 +141,54 @@ impl<'a> System<'a> for MeleeCombatSystem {
armour_ac_bonus += ac.amount;
}
}
let armour_class = bac - attribute_ac_bonus - skill_ac_bonus - armour_ac_bonus;
let mut armour_class = bac - attribute_ac_bonus - skill_ac_bonus - armour_ac_bonus;
let target_number = 10 + armour_class + attacker_pools.level;
if armour_class < 0 {
armour_class = rng.roll_dice(1, armour_class);
}
if d20 != 1 && (d20 == 20 || modified_hit_roll < target_number) {
let target_number = 10 + armour_class + attacker_bonuses;
if COMBAT_LOGGING {
rltk::console::log(format!(
"ATTACKLOG: {} *ATTACKED* {}: rolled ({}) 1d20 vs. {} (10 + {}AC + {}to-hit)",
&name.name, &target_name.name, d20, target_number, armour_class, attacker_bonuses
));
}
if d20 < 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 attribute_damage_bonus = weapon_info.damage_bonus;
let mut damage =
i32::max(0, base_damage + attribute_damage_bonus + skill_damage_bonus + attribute_damage_bonus);
if COMBAT_LOGGING {
rltk::console::log(format!(
"ATTACKLOG: {} *{}* {} for {} ({}[{}d{}]+{}[skill]+{}[attr])",
&name.name,
attack_verb,
&target_name.name,
damage,
base_damage,
weapon_info.damage_n_dice,
weapon_info.damage_die_type,
skill_damage_bonus,
attribute_damage_bonus
));
}
if armour_class < 0 {
let ac_damage_reduction = rng.roll_dice(1, armour_class);
damage = i32::min(1, damage - ac_damage_reduction);
if COMBAT_LOGGING {
rltk::console::log(format!(
"ATTACKLOG: {} reduced their damage taken by {} (1dAC), and took {} hp damage.",
&target_name.name, ac_damage_reduction, damage
));
}
}
let pos = positions.get(wants_melee.target);
if let Some(pos) = pos {
@ -168,6 +219,10 @@ impl<'a> System<'a> for MeleeCombatSystem {
.log();
}
} else {
if COMBAT_LOGGING {
rltk::console::log(format!("ATTACKLOG: {} *MISSED* {}", &name.name, &target_name.name,));
}
let pos = positions.get(wants_melee.target);
if let Some(pos) = pos {
particle_builder.attack_miss(pos.x, pos.y)

View file

@ -72,11 +72,17 @@ impl RawMaster {
}
}
pub fn spawn_named_entity(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
pub fn spawn_named_entity(
raws: &RawMaster,
ecs: &mut World,
key: &str,
pos: SpawnType,
map_difficulty: i32,
) -> Option<Entity> {
if raws.item_index.contains_key(key) {
return spawn_named_item(raws, ecs, key, pos);
} else if raws.mob_index.contains_key(key) {
return spawn_named_mob(raws, ecs, key, pos);
return spawn_named_mob(raws, ecs, key, pos, map_difficulty);
} else if raws.prop_index.contains_key(key) {
return spawn_named_prop(raws, ecs, key, pos);
}
@ -166,7 +172,13 @@ pub fn spawn_named_item(raws: &RawMaster, ecs: &mut World, key: &str, pos: Spawn
None
}
pub fn spawn_named_mob(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option<Entity> {
pub fn spawn_named_mob(
raws: &RawMaster,
ecs: &mut World,
key: &str,
pos: SpawnType,
map_difficulty: i32,
) -> Option<Entity> {
if raws.mob_index.contains_key(key) {
let mob_template = &raws.raws.mobs[raws.mob_index[key]];
@ -228,7 +240,20 @@ pub fn spawn_named_mob(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnT
}
}
eb = eb.with(attr);
let mob_level = if mob_template.level.is_some() { mob_template.level.unwrap() } else { 1 };
let base_mob_level = if mob_template.level.is_some() { mob_template.level.unwrap() } else { 0 };
let mut mob_level = base_mob_level;
let difficulty = 0;
if base_mob_level > difficulty {
mob_level -= 1;
} else if base_mob_level < difficulty {
mob_level += (difficulty - base_mob_level) / 5;
if mob_level as f32 > 1.5 * base_mob_level as f32 {
let mob_levelf32 = (1.5 * base_mob_level as f32).trunc();
mob_level = mob_levelf32 as i32;
}
}
// Should really use existing RNG here
let mut rng = rltk::RandomNumberGenerator::new();
@ -290,7 +315,7 @@ pub fn spawn_named_mob(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnT
// Build entity, then check for anything they're wearing
if let Some(wielding) = &mob_template.equipped {
for tag in wielding.iter() {
spawn_named_entity(raws, ecs, tag, SpawnType::Equipped { by: new_mob });
spawn_named_entity(raws, ecs, tag, SpawnType::Equipped { by: new_mob }, map_difficulty);
}
}
return Some(new_mob);

View file

@ -62,8 +62,15 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity {
ecs,
"equip_dagger",
raws::SpawnType::Equipped { by: player },
0,
);
raws::spawn_named_entity(
&raws::RAWS.lock().unwrap(),
ecs,
"food_apple",
raws::SpawnType::Carried { by: player },
0,
);
raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, "food_apple", raws::SpawnType::Carried { by: player });
return player;
}
@ -143,13 +150,19 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize],
/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)
pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) {
let map = ecs.fetch::<Map>();
let map_difficulty = map.difficulty;
let width = map.width as usize;
let x = (*spawn.0 % width) as i32;
let y = (*spawn.0 / width) as i32;
std::mem::drop(map);
let spawn_result =
raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, &spawn.1, raws::SpawnType::AtPosition { x, y });
let spawn_result = raws::spawn_named_entity(
&raws::RAWS.lock().unwrap(),
ecs,
&spawn.1,
raws::SpawnType::AtPosition { x, y },
map_difficulty,
);
if spawn_result.is_some() {
return;
}