From 6e6d364aa5bd17b766720f005612bd1804ee4c26 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Fri, 28 Jul 2023 17:12:02 +0100 Subject: [PATCH] fixed some attributes double-dipping, and added COMBAT_LOGGING --- raws/mobs.json | 6 ++- src/gamesystem.rs | 3 ++ src/melee_combat_system.rs | 75 +++++++++++++++++++++++++++++++++----- src/raws/rawmaster.rs | 35 +++++++++++++++--- src/spawner.rs | 19 ++++++++-- 5 files changed, 119 insertions(+), 19 deletions(-) diff --git a/raws/mobs.json b/raws/mobs.json index df8d62f..df6b2f4 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -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": [ diff --git a/src/gamesystem.rs b/src/gamesystem.rs index 81b676b..e682458 100644 --- a/src/gamesystem.rs +++ b/src/gamesystem.rs @@ -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); diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 51e9452..a6c58f0 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -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) diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 56fdb88..8575d43 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -72,11 +72,17 @@ impl RawMaster { } } -pub fn spawn_named_entity(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option { +pub fn spawn_named_entity( + raws: &RawMaster, + ecs: &mut World, + key: &str, + pos: SpawnType, + map_difficulty: i32, +) -> Option { 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 { +pub fn spawn_named_mob( + raws: &RawMaster, + ecs: &mut World, + key: &str, + pos: SpawnType, + map_difficulty: i32, +) -> Option { 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); diff --git a/src/spawner.rs b/src/spawner.rs index 00b71e2..e213390 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -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::(); + 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; }