diff --git a/src/damage.hpp b/src/damage.hpp index b4a0f9e4..efa8ec2f 100644 --- a/src/damage.hpp +++ b/src/damage.hpp @@ -21,10 +21,9 @@ enum class eDamageType { COLD = 5, UNDEAD = 6, DEMON = 7, - // Acid is treated as magic damage but needs a value to prevent it using a zap explosion - ACID = -1, + ACID = 8, // Keep these two last - SPECIAL = 8, // Completely unblockable damage from assassination skill + SPECIAL = 9, // Completely unblockable damage from assassination skill MARKED = 10, }; diff --git a/src/fileio/estreams.cpp b/src/fileio/estreams.cpp index bfb457cb..fbc87592 100644 --- a/src/fileio/estreams.cpp +++ b/src/fileio/estreams.cpp @@ -371,7 +371,7 @@ std::istream& operator >> (std::istream& in, eFieldType& e) { // MARK: eDamageType cEnumLookup dmg_names = { - "weap", "fire", "poison", "magic", "weird", "cold", "undead", "demon", "spec", + "weap", "fire", "poison", "magic", "weird", "cold", "undead", "demon", "acid", "spec", }; std::ostream& operator << (std::ostream& out, eDamageType e) { diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index e1fe475e..17438af3 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -118,7 +118,9 @@ std::map> feature_flags = { {"talk-go-back", {"StackV1"}}, // Bugs required for several VoDT test replays to run faithfully {"empty-wandering-monster-bug", {"fixed"}}, - {"too-many-extra-wandering-monsters-bug", {"fixed"}} + {"too-many-extra-wandering-monsters-bug", {"fixed"}}, + // Game balance + {"magic-resistance", {"fixed"}} // Resist Magic used to not help with magic damage! }; struct cParseEntrance { diff --git a/src/game/boe.party.cpp b/src/game/boe.party.cpp index 3600d582..c7fe3aa0 100644 --- a/src/game/boe.party.cpp +++ b/src/game/boe.party.cpp @@ -2363,12 +2363,9 @@ short damage_pc(cPlayer& which_pc,short how_much,eDamageType damage_type,eRace t int boom_type = boom_gr[damage_type]; - // Acid doesn't actually have its own damage type in classic BoE - if(damage_type == eDamageType::ACID) - damage_type = eDamageType::MAGIC; - // armor - if(damage_type == eDamageType::WEAPON || damage_type == eDamageType::UNDEAD || damage_type == eDamageType::DEMON) { + static std::set armor_resist_damage = { eDamageType::WEAPON, eDamageType::UNDEAD, eDamageType::DEMON }; + if(armor_resist_damage.count(damage_type)) { how_much -= minmax(-5,5,which_pc.status[eStatus::BLESS_CURSE]); for(short i = 0; i < cPlayer::INVENTORY_SIZE; i++) { const cItem& item = which_pc.items[i]; @@ -2416,6 +2413,10 @@ short damage_pc(cPlayer& which_pc,short how_much,eDamageType damage_type,eRace t } short prot_from_dmg = which_pc.get_prot_level(eItemAbil::DAMAGE_PROTECTION,int(damage_type)); + // Acid damage used to be magic damage, so magic protection counts as acid protection: + if(damage_type == eDamageType::ACID){ + prot_from_dmg += which_pc.get_prot_level(eItemAbil::DAMAGE_PROTECTION,int(eDamageType::MAGIC)); + } if(prot_from_dmg > 0) { // TODO: Why does this not depend on the ability strength if it's not weapon damage? if(damage_type == eDamageType::WEAPON) how_much -= prot_from_dmg; @@ -2447,8 +2448,13 @@ short damage_pc(cPlayer& which_pc,short how_much,eDamageType damage_type,eRace t how_much = 0; // Mag. res helps w. fire and cold - // TODO: Why doesn't this help with magic damage!? - if(damage_type == eDamageType::FIRE || damage_type == eDamageType::COLD) { + static std::set magic_resist_damage = { eDamageType::FIRE, eDamageType::COLD }; + // Now it also helps with MAGIC: + if(has_feature_flag("magic-resistance", "fixed")){ + magic_resist_damage.insert(eDamageType::MAGIC); + magic_resist_damage.insert(eDamageType::ACID); + } + if(magic_resist_damage.count(damage_type)) { int magic_res = which_pc.status[eStatus::MAGIC_RESISTANCE]; if(magic_res > 0) how_much /= 2; @@ -2458,7 +2464,8 @@ short damage_pc(cPlayer& which_pc,short how_much,eDamageType damage_type,eRace t // major resistance short full_prot = which_pc.get_prot_level(eItemAbil::FULL_PROTECTION); - if((damage_type == eDamageType::FIRE || damage_type == eDamageType::POISON || damage_type == eDamageType::MAGIC || damage_type == eDamageType::COLD) + std::set major_resist_damage = { eDamageType::FIRE, eDamageType::POISON, eDamageType::MAGIC, eDamageType::ACID, eDamageType::COLD}; + if(major_resist_damage.count(damage_type) && (full_prot > 0)) how_much = how_much / ((full_prot >= 7) ? 4 : 2); diff --git a/src/game/boe.specials.cpp b/src/game/boe.specials.cpp index 78498e51..fe1d0a95 100644 --- a/src/game/boe.specials.cpp +++ b/src/game/boe.specials.cpp @@ -338,7 +338,7 @@ bool check_special_terrain(location where_check,eSpecCtx mode,cPlayer& which_pc, break; if(mode == eSpecCtx::OUT_MOVE && out_boat_there(where_check)) break; - if(ter_flag3 > 0 && ter_flag3 < 8) + if(ter_flag3 > 0 && ter_flag3 < int(eDamageType::SPECIAL)) dam_type = (eDamageType) ter_flag3; else dam_type = eDamageType::WEAPON; r1 = get_ran(ter_flag2,1,ter_flag1); @@ -356,6 +356,10 @@ bool check_special_terrain(location where_check,eSpecCtx mode,cPlayer& which_pc, add_string_to_buf(" You feel cold!"); pic_type = 4; break; + case eDamageType::ACID: + add_string_to_buf(" It burns!"); + pic_type = 6; + break; case eDamageType::SPECIAL: dam_type = eDamageType::UNBLOCKABLE; BOOST_FALLTHROUGH; @@ -1472,16 +1476,13 @@ short damage_monst(cCreature& victim, short who_hit, short how_much, eDamageType int boom_type = boom_gr[dam_type]; - // Acid doesn't actually have its own damage type in classic BoE - if(dam_type == eDamageType::ACID) - dam_type = eDamageType::MAGIC; - if(dam_type < eDamageType::SPECIAL) { how_much = percent(how_much, victim.resist[dam_type]); } // Absorb damage? - if((dam_type == eDamageType::FIRE || dam_type == eDamageType::MAGIC || dam_type == eDamageType::COLD) + std::set absorbable_damage = { eDamageType::FIRE, eDamageType::MAGIC, eDamageType::COLD, eDamageType::ACID }; + if(absorbable_damage.count(dam_type) && victim.abil[eMonstAbil::ABSORB_SPELLS].active && get_ran(1,1,1000) <= victim.abil[eMonstAbil::ABSORB_SPELLS].special.extra1) { add_check_overflow(victim.health, how_much); ASB(" Magic absorbed."); @@ -1491,7 +1492,7 @@ short damage_monst(cCreature& victim, short who_hit, short how_much, eDamageType // Saving throw if((dam_type == eDamageType::FIRE || dam_type == eDamageType::COLD) && get_ran(1,0,20) <= victim.level) how_much /= 2; - if(dam_type == eDamageType::MAGIC && (get_ran(1,0,24) <= victim.level)) + if((dam_type == eDamageType::MAGIC || dam_type == eDamageType::ACID) && (get_ran(1,0,24) <= victim.level)) how_much /= 2; // Invulnerable? @@ -1501,8 +1502,13 @@ short damage_monst(cCreature& victim, short who_hit, short how_much, eDamageType how_much /= 10; // Mag. res helps w. fire and cold - // TODO: Why doesn't this help with magic damage!? - if(dam_type == eDamageType::FIRE || dam_type == eDamageType::COLD) { + static std::set magic_resist_damage = { eDamageType::FIRE, eDamageType::COLD }; + // Now it also helps with MAGIC: + if(has_feature_flag("magic-resistance", "fixed")){ + magic_resist_damage.insert(eDamageType::MAGIC); + magic_resist_damage.insert(eDamageType::ACID); + } + if(magic_resist_damage.count(dam_type)) { int magic_res = victim.status[eStatus::MAGIC_RESISTANCE]; if(magic_res > 0) how_much /= 2; diff --git a/src/scenario/item.cpp b/src/scenario/item.cpp index 7e7161bb..7fd549ee 100644 --- a/src/scenario/item.cpp +++ b/src/scenario/item.cpp @@ -1037,6 +1037,7 @@ std::string cItem::getAbilName() const { case eDamageType::WEAPON: sout << "Enhanced"; break; case eDamageType::UNDEAD: sout << "Necrotic"; break; case eDamageType::DEMON: sout << "Unholy"; break; + case eDamageType::ACID: sout << "Acid"; break; case eDamageType::SPECIAL: case eDamageType::UNBLOCKABLE: sout << "Dark"; break; case eDamageType::MARKED: break; // Invalid @@ -1079,6 +1080,7 @@ std::string cItem::getAbilName() const { case eDamageType::MAGIC: sout << "in sparks"; break; case eDamageType::POISON: sout << "into slime"; break; case eDamageType::WEAPON: sout << "in shrapnel"; break; + case eDamageType::ACID: sout << "with acid"; break; case eDamageType::SPECIAL: case eDamageType::UNBLOCKABLE: sout << "in darkness"; break; case eDamageType::UNDEAD: sout.str("Implodes"); break; @@ -1117,6 +1119,7 @@ std::string cItem::getAbilName() const { case eDamageType::DEMON: sout << "Demon"; break; case eDamageType::UNDEAD: sout << "Undead"; break; case eDamageType::POISON: sout << "Poison"; break; + case eDamageType::ACID: sout << "Acid"; break; case eDamageType::SPECIAL: case eDamageType::UNBLOCKABLE: sout << "Darkness"; break; case eDamageType::MARKED: break; // Invalid diff --git a/src/scenario/monster.cpp b/src/scenario/monster.cpp index bc10862e..1c7af6db 100644 --- a/src/scenario/monster.cpp +++ b/src/scenario/monster.cpp @@ -395,7 +395,7 @@ std::map::iterator cMonster::addAbil(eMonstAbilTemplate wha } cMonster::cMonster(){ - for(int i = 0; i <= 8; i++) { + for(int i = 0; i <= int(eDamageType::SPECIAL); i++) { eDamageType dmg = eDamageType(i); resist[dmg] = 100; } @@ -560,6 +560,7 @@ std::string uAbility::to_string(eMonstAbil key) const { case eDamageType::FIRE: sout << "Fiery"; break; case eDamageType::COLD: sout << "Icy"; break; case eDamageType::MAGIC: sout << "Shock"; break; + case eDamageType::ACID: sout << "Acid"; break; case eDamageType::SPECIAL: case eDamageType::UNBLOCKABLE: sout << "Wounding"; break; case eDamageType::POISON: sout << "Pain"; break; diff --git a/src/scenedit/scen.fileio.cpp b/src/scenedit/scen.fileio.cpp index 3270fbe1..cf61b52c 100644 --- a/src/scenedit/scen.fileio.cpp +++ b/src/scenedit/scen.fileio.cpp @@ -595,7 +595,7 @@ void writeMonstersToXml(ticpp::Printer&& data, cScenario& scenario) { data.CloseElement("attacks"); data.OpenElement("immunity"); - for(int i = 0; i < 8; i++) { + for(int i = 0; i < int(eDamageType::SPECIAL); i++) { eDamageType dmg = eDamageType(i); if(monst.resist[dmg] != 100) data.PushElement(boost::lexical_cast(dmg), monst.resist[dmg]); diff --git a/src/scenedit/scen.graphics.cpp b/src/scenedit/scen.graphics.cpp index d6478e38..5a4c95cb 100644 --- a/src/scenedit/scen.graphics.cpp +++ b/src/scenedit/scen.graphics.cpp @@ -88,6 +88,7 @@ extern rectangle terrain_rects[256]; unsigned char small_what_drawn[64][64]; extern bool small_any_drawn; +// These are at the bottom of edbuttons.png: static short get_small_icon(ter_num_t ter){ short icon = -1; switch(scenario.ter_types[ter].special){ @@ -108,6 +109,9 @@ static short get_small_icon(ter_num_t ter){ case eDamageType::POISON: icon = 17; break; + case eDamageType::ACID: + icon = 24; // green with black spots, doesn't seem to be used elsewhere + break; case eDamageType::MAGIC: icon = 20; break; diff --git a/src/scenedit/scen.keydlgs.cpp b/src/scenedit/scen.keydlgs.cpp index 84800195..c65b0b7b 100644 --- a/src/scenedit/scen.keydlgs.cpp +++ b/src/scenedit/scen.keydlgs.cpp @@ -221,6 +221,7 @@ static void set_pattern(cTilemap& map, const effect_pat_type& pat) { case eDamageType::WEAPON: clr = Colours::MAROON; break; case eDamageType::FIRE: clr = Colours::RED; break; case eDamageType::POISON: clr = Colours::GREEN; break; + case eDamageType::ACID: clr = Colours::LIGHT_GREEN; break; // Distinct enough from green? case eDamageType::MAGIC: clr = Colours::PURPLE; break; case eDamageType::UNBLOCKABLE: clr = Colours::LIGHT_BLUE; break; case eDamageType::COLD: clr = Colours::BLUE; break; @@ -975,8 +976,8 @@ short choose_field_type(short cur, cDialog* parent, bool includeSpec) { } pic_num_t choose_damage_type(short cur, cDialog* parent, bool allow_spec) { - static const char*const damageNames[] = {"Weapon", "Fire", "Poison", "Magic", "Weird", "Cold", "Undead", "Demon", "Unblockable"}; - static const std::vector pics = {3,0,2,1,5,4,3,3,1}; + static const char*const damageNames[] = {"Weapon", "Fire", "Poison", "Magic", "Weird", "Cold", "Undead", "Demon", "Acid", "Unblockable"}; + static const std::vector pics = {3,0,2,1,5,4,3,3,6,1}; short prev = cur; if(cur < 0 || cur >= pics.size()) cur = 0; cPictChoice pic_dlg(pics.begin(), pics.end() - !allow_spec, PIC_BOOM, parent); diff --git a/test/init.cpp b/test/init.cpp index 48cbda46..0b5751bc 100644 --- a/test/init.cpp +++ b/test/init.cpp @@ -68,7 +68,7 @@ TEST_CASE("Initialization sanity test for monster") { CHECK(monst.abil.empty()); CHECK(monst.corpse_item == 0); CHECK(monst.corpse_item_chance == 0); - CHECK(monst.resist.size() == 10); + CHECK(monst.resist.size() == 11); CHECK(monst.resist[eDamageType::WEAPON] == 100); CHECK(monst.resist[eDamageType::FIRE] == 100); CHECK(monst.resist[eDamageType::POISON] == 100); @@ -77,6 +77,7 @@ TEST_CASE("Initialization sanity test for monster") { CHECK(monst.resist[eDamageType::UNBLOCKABLE] == 100); CHECK(monst.resist[eDamageType::UNDEAD] == 100); CHECK(monst.resist[eDamageType::DEMON] == 100); + CHECK(monst.resist[eDamageType::ACID] == 100); CHECK(monst.resist[eDamageType::MARKED] == 100); CHECK_FALSE(monst.mindless); CHECK_FALSE(monst.invuln); diff --git a/test/monst_write.cpp b/test/monst_write.cpp index aa2bfc42..a639e856 100644 --- a/test/monst_write.cpp +++ b/test/monst_write.cpp @@ -132,7 +132,8 @@ TEST_CASE("Saving monster types") { scen.scen_monsters[1].resist[eDamageType::COLD] = 30; scen.scen_monsters[1].resist[eDamageType::UNDEAD] = 35; scen.scen_monsters[1].resist[eDamageType::DEMON] = 40; - scen.scen_monsters[1].resist[eDamageType::SPECIAL] = 45; + scen.scen_monsters[1].resist[eDamageType::ACID] = 45; + scen.scen_monsters[1].resist[eDamageType::SPECIAL] = 50; in_and_out("resistance", scen); CHECK(scen.scen_monsters[1].resist[eDamageType::WEAPON] == 5); CHECK(scen.scen_monsters[1].resist[eDamageType::FIRE] == 10); @@ -142,6 +143,7 @@ TEST_CASE("Saving monster types") { CHECK(scen.scen_monsters[1].resist[eDamageType::COLD] == 30); CHECK(scen.scen_monsters[1].resist[eDamageType::UNDEAD] == 35); CHECK(scen.scen_monsters[1].resist[eDamageType::DEMON] == 40); + CHECK(scen.scen_monsters[1].resist[eDamageType::ACID] == 45); // This one should not be saved, so we expect it to revert to default CHECK(scen.scen_monsters[1].resist[eDamageType::SPECIAL] == 100); }