diff --git a/HeavenClient/Data/EquipData.cpp b/HeavenClient/Data/EquipData.cpp index 64078908d..2128044ad 100644 --- a/HeavenClient/Data/EquipData.cpp +++ b/HeavenClient/Data/EquipData.cpp @@ -32,6 +32,18 @@ namespace ms slots = src["tuc"]; recovery = src["recovery"].get_real(1.0); + // A field of 0 or less makes the spell do nothing. + int16_t elemdefault = static_cast(src["elemDefault"].get_integer(100)); + const char *boosts[] = { "", "incRMAI", "incRMAF", "incRMAL", "incRMAS", "incRMAH", "", "" }; + + for (size_t element = 0; element < elemboost.size(); element++) + { + int16_t boost = *boosts[element] + ? static_cast(src[boosts[element]].get_integer(elemdefault)) + : 100; + elemboost[element] = boost > 0 ? boost : 0; + } + reqstats[MapleStat::Id::LEVEL] = src["reqLevel"]; reqstats[MapleStat::Id::JOB] = src["reqJob"]; reqstats[MapleStat::Id::STR] = src["reqSTR"]; @@ -180,4 +192,9 @@ namespace ms { return recovery; } + + int16_t EquipData::get_elemboost(uint8_t element) const + { + return element < elemboost.size() ? elemboost[element] : 100; + } } diff --git a/HeavenClient/Data/EquipData.h b/HeavenClient/Data/EquipData.h index cb6c3ecfc..3da952e45 100644 --- a/HeavenClient/Data/EquipData.h +++ b/HeavenClient/Data/EquipData.h @@ -17,6 +17,7 @@ ////////////////////////////////////////////////////////////////////////////////// #pragma once +#include #include #include @@ -62,6 +63,12 @@ namespace ms // recover faster (info/recovery, 1 when absent). double get_recovery() const; + // The percent of a spell's damage a weapon gives for its element + // (FUN_005d284f): incRMAI, F, L, S or H for ice, fire, lightning, + // poison and holy, else elemDefault, else 100. Other elements get + // 100. + int16_t get_elemboost(uint8_t element) const; + private: // Allow the cache to use the constructor friend Cache; @@ -79,5 +86,6 @@ namespace ms bool cash; bool tradeblock; double recovery; + std::array elemboost; }; } \ No newline at end of file diff --git a/HeavenClient/Gameplay/Combat/Attack.h b/HeavenClient/Gameplay/Combat/Attack.h index b0b70372c..51838f78a 100644 --- a/HeavenClient/Gameplay/Combat/Attack.h +++ b/HeavenClient/Gameplay/Combat/Attack.h @@ -61,6 +61,8 @@ namespace ms int32_t mastery = 0; int32_t amplify = 100; uint8_t element = 0; + // The weapon's percent for that element (an Elemental Wand or Staff). + int32_t elemboost = 100; // Heal's `hp` percent, which it hurts the undead with in place of a // magic attack, and how many it reaches, mobs and party members. int32_t heal = 0; diff --git a/HeavenClient/Gameplay/Combat/Combat.cpp b/HeavenClient/Gameplay/Combat/Combat.cpp index a0cbbc505..dd5747986 100644 --- a/HeavenClient/Gameplay/Combat/Combat.cpp +++ b/HeavenClient/Gameplay/Combat/Combat.cpp @@ -1381,7 +1381,9 @@ namespace ms // nothing. A spell flies one ball to each mob it hits. A mob's // hits all land 1.5 ms per pixel it is away across, or along a // spell's path, after the attack's hit frame, but for a few - // skills (0x954867). + // skills (0x954867). Energy Orb's ball instead jumps from 25 px + // ahead to the middle of each mob in turn, each hit when it gets + // there (FUN_0094289f). Animation animation = move.get_bullet(user, result.bullet); Point origin = shot_path::origin(user.get_position(), reach, result.toleft); Point start = shot_path::bullet_start(result.skill, origin, result.toleft); @@ -1396,7 +1398,7 @@ namespace ms if (spell) count = sets = 1; - else if (shot_path::hides_bullet(result.skill)) + else if (shot_path::hides_bullet(result.skill) || shot_path::hops(result.skill)) return; uint8_t per_set = std::max(count / sets, 1); @@ -1413,6 +1415,9 @@ namespace ms uint16_t first = 0; std::optional> first_hit; std::vector> bodies; + bool hops = !spell && shot_path::hops(result.skill); + Point hop_from = shot_path::hop_start(origin, result.toleft); + uint16_t hopped = 0; for (size_t target = 0; target < result.damagelines.size(); target++) { @@ -1427,6 +1432,16 @@ namespace ms uint16_t hit_delay = spell ? shot_path::flight_time(origin, hit) : shot_path::target_delay(result.skill, target, first, origin, hit); + if (hops) + { + Point hop_to = shot_path::hop_point(body); + uint16_t hop = shot_path::hop_time(result.skill, hop_from, hop_to); + bulleteffects.push(delay + hopped, Bullet(animation, hop_from, hop_to, hop)); + hopped += hop; + hit_delay = hopped; + hop_from = hop_to; + } + if (target == 0) first = hit_delay; @@ -1644,6 +1659,8 @@ namespace ms uint16_t first = 0; std::optional> first_hit; std::vector> bodies; + std::optional> hop_from; + uint16_t hopped = 0; for (size_t target = 0; target < result.damagelines.size(); target++) { @@ -1673,6 +1690,20 @@ namespace ms Point aim = remote_spell::aim(start, body, left); uint16_t hit = remote_shot::hit_delay(skill, target, first, delay, start, aim); + // Energy Orb's ball jumps from 25 px ahead of where it starts + // to the middle of each mob in turn, as the user's own does + // (0x981ba6). + if (shot_path::hops(skill)) + { + Point from = hop_from ? *hop_from : shot_path::hop_start(start, result.toleft); + Point to = shot_path::hop_point(body); + uint16_t hop = shot_path::hop_time(skill, from, to); + bulleteffects.push(delay + hopped, Bullet(animation, from, to, hop)); + hopped += hop; + hit = hopped; + hop_from = to; + } + if (target == 0) first = hit; diff --git a/HeavenClient/Gameplay/Combat/MagicDamage.h b/HeavenClient/Gameplay/Combat/MagicDamage.h index bf373bc36..68c661ba5 100644 --- a/HeavenClient/Gameplay/Combat/MagicDamage.h +++ b/HeavenClient/Gameplay/Combat/MagicDamage.h @@ -163,6 +163,33 @@ namespace ms return damage - between(mdef * 0.5, mdef * 0.6, roll); } + // Whether a spell gets through a Magic Immunity: a roll of 0 to 99 + // must be at most the player's chance to pierce it, so 1 in 100 + // does with none. One that doesn't hits for 1. + inline bool pierces(int32_t roll, int32_t chance) + { + return roll <= chance; + } + + // A percent of the hit: Magic Guard Up's n, or the weapon's for + // the element. + inline double percent(double damage, int32_t percent) + { + return percent * damage * 0.01; + } + + // Magic Guard Up leaves n% of the hit after the defense. + inline double guarded(double damage, int32_t guard) + { + return guard ? percent(damage, guard) : damage; + } + + // Weakness makes the hit n% harder. + inline double weakened(double damage, int32_t weakness) + { + return weakness ? percent(damage, weakness + 100) : damage; + } + // The number shown: between 1 and 199999, rounded down. inline int32_t finish(double damage) { diff --git a/HeavenClient/Gameplay/Combat/RemoteShot.h b/HeavenClient/Gameplay/Combat/RemoteShot.h index d363bc2b2..c8b0a6b1a 100644 --- a/HeavenClient/Gameplay/Combat/RemoteShot.h +++ b/HeavenClient/Gameplay/Combat/RemoteShot.h @@ -138,13 +138,6 @@ namespace ms } } - // Whether the shot's bullets jump from mob to mob instead: Energy Orb - // and Spark. - inline bool hops(int32_t skill) - { - return skill == 5121002 || skill == 15111006; - } - // Whether the shot flies its bullets to every mob it hits, rather // than to the first alone. inline bool shoots_all(int32_t skill) @@ -171,7 +164,7 @@ namespace ms inline bool shoots(int32_t skill, size_t target, Rectangle lt_rb, int16_t start_x, Rectangle body, bool left) { - if (hides_bullet(skill) || hops(skill)) + if (hides_bullet(skill) || shot_path::hops(skill)) return false; if (target > 0 && !shoots_all(skill)) @@ -196,7 +189,7 @@ namespace ms // than hide them otherwise. inline bool hides_miss(int32_t skill) { - return shot_path::hides_bullet(skill) || hops(skill); + return shot_path::hides_bullet(skill) || shot_path::hops(skill); } // Where the bullets of a shot that hit nothing start, in front of diff --git a/HeavenClient/Gameplay/Combat/ShotPath.h b/HeavenClient/Gameplay/Combat/ShotPath.h index 4d5a46d58..f629e0567 100644 --- a/HeavenClient/Gameplay/Combat/ShotPath.h +++ b/HeavenClient/Gameplay/Combat/ShotPath.h @@ -473,6 +473,40 @@ namespace ms return static_cast(MS_PER_PIXEL * std::sqrt(dx * dx + dy * dy)); } + // Whether a shot's ball jumps from mob to mob instead, each hit when + // it gets there: Energy Orb and Spark (FUN_0094289f). + inline bool hops(int32_t skill) + { + return skill == 5121002 || skill == 15111006; + } + + // Where the ball's first jump starts: 25 px ahead of `origin`. + inline Point hop_start(Point origin, bool toleft) + { + return { static_cast(origin.x() + (toleft ? -25 : 25)), origin.y() }; + } + + // Where the ball jumps to on a mob with these bounds: their middle. + inline Point hop_point(Rectangle body) + { + return { + static_cast((body.left() + body.right()) / 2), + static_cast((body.top() + body.bottom()) / 2) + }; + } + + // How long the ball's jump from `from` to `to` takes: 1.25 ms per + // pixel, Spark's 2. Each mob is hit when the ball gets there, a jump + // after the last. + inline uint16_t hop_time(int32_t skill, Point from, Point to) + { + double dx = to.x() - from.x(); + double dy = to.y() - from.y(); + double per_pixel = skill == 15111006 ? 2.0 : 1.25; + + return static_cast(per_pixel * std::sqrt(dx * dx + dy * dy)); + } + // How long after the attack's hit frame the damage at `hit` lands. // It only counts the horizontal distance. inline uint16_t hit_delay(Point origin, Point hit) diff --git a/HeavenClient/Gameplay/Combat/Skill.cpp b/HeavenClient/Gameplay/Combat/Skill.cpp index db3e0d0ea..bcaeb8806 100644 --- a/HeavenClient/Gameplay/Combat/Skill.cpp +++ b/HeavenClient/Gameplay/Combat/Skill.cpp @@ -20,7 +20,9 @@ #include "Heal.h" #include "Prepare.h" +#include "../../Character/Char.h" #include "../../Character/SkillId.h" +#include "../../Data/EquipData.h" #include "../../Data/SkillData.h" #include "../../Util/Misc.h" @@ -28,6 +30,17 @@ namespace ms { + namespace + { + // The percent the user's weapon gives a spell of `element`. + int32_t elemboost(const Char &user, uint8_t element) + { + int32_t weapon = user.get_look().get_equips().get_weapon(); + + return weapon > 0 ? EquipData::get(weapon).get_elemboost(element) : 100; + } + } + Skill::Skill(int32_t id) : skillid(id) { const SkillData &data = SkillData::get(skillid); @@ -229,12 +242,14 @@ namespace ms { attack.heal = stats.hp; attack.element = SkillData::get(skillid).get_element(); + attack.elemboost = elemboost(user, attack.element); attack.damagetype = Attack::DMG_MAGIC; } else if (stats.matk) { attack.matk += stats.matk; attack.mastery = stats.mastery; attack.element = SkillData::get(skillid).get_element(); + attack.elemboost = elemboost(user, attack.element); attack.damagetype = Attack::DMG_MAGIC; } else { diff --git a/HeavenClient/Gameplay/MapleMap/Mob.cpp b/HeavenClient/Gameplay/MapleMap/Mob.cpp index 68f15fa2e..f119bb4e8 100644 --- a/HeavenClient/Gameplay/MapleMap/Mob.cpp +++ b/HeavenClient/Gameplay/MapleMap/Mob.cpp @@ -23,6 +23,7 @@ #include "../Combat/HitPoint.h" #include "../Combat/MonsterMagnet.h" #include "../Combat/Prepare.h" +#include "../../Character/SkillId.h" #include "../../Constants.h" #include "../../Util/Misc.h" #include "../../Util/NxFiles.h" @@ -1992,7 +1993,9 @@ namespace ms float Mob::calculate_hitchance(int16_t leveldelta, int32_t player_accuracy) const { float faccuracy = static_cast(player_accuracy); - float hitchance = faccuracy / (((1.84f + 0.07f * leveldelta) * avoid) + 1.0f); + // A monster's EVA status adds to its avoid (FUN_0078df87). + int32_t evasion = avoid + status_value(MobStatParser::EVA); + float hitchance = faccuracy / (((1.84f + 0.07f * leveldelta) * evasion) + 1.0f); if (hitchance < 0.01f) hitchance = 0.01f; @@ -2000,16 +2003,22 @@ namespace ms return hitchance; } + // A monster's PDR status adds to its weapon defense. + int32_t Mob::defense() const + { + return wdef + status_value(MobStatParser::PDR); + } + double Mob::calculate_mindamage(int16_t leveldelta, double damage) const { - double mindamage = damage * (1 - 0.01 * leveldelta) - wdef * 0.6; + double mindamage = damage * (1 - 0.01 * leveldelta) - defense() * 0.6; return mindamage < 1.0 ? 1.0 : mindamage; } double Mob::calculate_maxdamage(int16_t leveldelta, double damage) const { - double maxdamage = damage * (1 - 0.01 * leveldelta) - wdef * 0.5; + double maxdamage = damage * (1 - 0.01 * leveldelta) - defense() * 0.5; return maxdamage < 1.0 ? 1.0 : maxdamage; } @@ -2069,6 +2078,18 @@ namespace ms std::pair Mob::next_damage(double mindamage, double maxdamage, float hitchance, float critical, const Attack &attack) const { + using namespace magic_damage; + + // A blow meets the same statuses a spell does (FUN_0078df87), but + // fixed damage skips them. + bool weapon = attack.damagetype == Attack::DamageType::DMG_WEAPON; + + if (weapon && status_value(MobStatParser::DISABLE)) + return std::pair(0, false); + + if (weapon && status_value(MobStatParser::PIMMUNE) && !pierces(randomizer.next_int(100), 0)) + return std::pair(1, false); + bool hit = randomizer.below(hitchance); if (!hit) @@ -2082,9 +2103,17 @@ namespace ms if (iscritical) damage *= 1.5; + // Sacrifice goes through a Power Guard Up. + if (weapon && attack.skill != static_cast(SkillId::SACRIFICE)) + damage = guarded(damage, status_value(MobStatParser::PGUARD_UP)); + // A held skill hits harder the longer it was charged (0x79048c). damage = prepare::charge_percent(attack.skill, attack.charge) * damage * 0.01; + // Only a critical hit gets through Hard Skin (0x7904ba). + if (weapon && status_value(MobStatParser::HARD_SKIN) && !iscritical) + return std::pair(0, false); + if (damage < 1) damage = 1; else if (damage > DAMAGECAP) @@ -2099,10 +2128,18 @@ namespace ms { using namespace magic_damage; + // Disable turns every hit away, and Magic Immunity all but 1 in + // 100 to 1 damage (FUN_00791617). + if (status_value(MobStatParser::DISABLE)) + return 0; + + if (status_value(MobStatParser::MIMMUNE) && !pierces(randomizer.next_int(100), 0)) + return 1; + double hitroll = randomizer.next_real(1.0); double accuracy = magic_damage::accuracy(attack.intelligence, attack.luck, attack.playerlevel, level); - if (!hits(accuracy, avoid, hitroll)) + if (!hits(accuracy, avoid + status_value(MobStatParser::EVA), hitroll)) return 0; double roll = randomizer.next_real(1.0); @@ -2111,9 +2148,17 @@ namespace ms : base(attack.magic, attack.intelligence, attack.mastery, attack.matk, roll); damage = damage * attack.amplify * 0.01; damage = elemental(damage, attack.skill, attack.element, elements); - damage = defend(damage, mdef, randomizer.next_real(1.0)); + damage = defend(damage, mdef + status_value(MobStatParser::MDR), randomizer.next_real(1.0)); + damage = guarded(damage, status_value(MobStatParser::MGUARD_UP)); damage = prepare::charge_percent(attack.skill, attack.charge) * damage * 0.01; + // Hard Skin stops all but critical spells, and only an Evan's are. + if (status_value(MobStatParser::HARD_SKIN)) + return 0; + + damage = percent(damage, attack.elemboost); + damage = weakened(damage, status_value(MobStatParser::WEAKNESS)); + return finish(damage); } diff --git a/HeavenClient/Gameplay/MapleMap/Mob.h b/HeavenClient/Gameplay/MapleMap/Mob.h index 0c5426db6..481b745b5 100644 --- a/HeavenClient/Gameplay/MapleMap/Mob.h +++ b/HeavenClient/Gameplay/MapleMap/Mob.h @@ -354,6 +354,9 @@ namespace ms // Calculate the hit chance float calculate_hitchance(int16_t leveldelta, int32_t accuracy) const; + // The weapon defense a blow meets. + int32_t defense() const; + // Calculate the minimum damage. double calculate_mindamage(int16_t leveldelta, double mindamage) const; diff --git a/HeavenClient/Net/Handlers/Helpers/MobStatParser.cpp b/HeavenClient/Net/Handlers/Helpers/MobStatParser.cpp index 4a4d59aa0..4cd6ed168 100644 --- a/HeavenClient/Net/Handlers/Helpers/MobStatParser.cpp +++ b/HeavenClient/Net/Handlers/Helpers/MobStatParser.cpp @@ -61,7 +61,12 @@ namespace ms recv.skip(4); if (flags.has(DISABLE)) - recv.skip(2); + { + int16_t value = static_cast(recv.read_byte()); + recv.skip(1); + + statuses.push_back({ DISABLE, value, 0, 0 }); + } return statuses; } diff --git a/HeavenClient/Net/Handlers/Helpers/MobStatParser.h b/HeavenClient/Net/Handlers/Helpers/MobStatParser.h index b9c084266..41ab9f078 100644 --- a/HeavenClient/Net/Handlers/Helpers/MobStatParser.h +++ b/HeavenClient/Net/Handlers/Helpers/MobStatParser.h @@ -50,12 +50,14 @@ namespace ms WEB = 17, PIMMUNE = 18, MIMMUNE = 19, + HARD_SKIN = 21, AMBUSH = 22, VENOM = 24, BURNED = 27, PCOUNTER = 29, MCOUNTER = 30, - DISABLE = 31 + DISABLE = 31, + WEAKNESS = 34 }; struct Status @@ -69,8 +71,8 @@ namespace ms }; // The statuses whose flags are set (FUN_0078b0b1), as MOB_STAT_SET - // and the spawn packets carry them. The burns, counters and - // Disable's data are read past. + // and the spawn packets carry them. The burns and counters are read + // past; Disable is kept with its first byte as its value. std::vector parse(InPacket& recv, const BuffParser::Flags& flags); // Read past the burns MOB_STAT_RESET ends (FUN_0078c70a). diff --git a/HeavenClient/tests/magic_damage.cpp b/HeavenClient/tests/magic_damage.cpp index f7f27a5ac..c169f4a9c 100644 --- a/HeavenClient/tests/magic_damage.cpp +++ b/HeavenClient/tests/magic_damage.cpp @@ -59,6 +59,17 @@ int main() // Magic defense takes 50% to 60% of itself off. require(near(defend(186.18138, 30, 0.0), 171.18138), "the low defense roll takes 15"); require(near(defend(294.71838, 30, 1.0), 276.71838), "the high one takes 18"); + // A monster's statuses: Magic Guard Up leaves n%, Weakness adds n%, + // and an Elemental Staff's percent scales the hit. + require(near(guarded(200.0, 30), 60.0), "Magic Guard Up 30 leaves 30%"); + require(near(guarded(200.0, 0), 200.0), "no guard leaves it whole"); + require(near(weakened(200.0, 20), 240.0), "Weakness 20 adds 20%"); + require(near(weakened(200.0, 0), 200.0), "no weakness adds nothing"); + require(near(percent(200.0, 125), 250.0) && near(percent(200.0, 75), 150.0), + "a staff's element percent"); + require(pierces(0, 0) && !pierces(1, 0), "only a roll of 0 pierces immunity with no chance"); + require(pierces(30, 30) && !pierces(31, 30), "a chance of n lets rolls up to n through"); + require(finish(171.18138) == 171, "the number shown rounds down"); require(finish(0.0) == 1 && finish(-40.0) == 1, "a hit shows at least 1"); require(finish(250000.0) == 199999, "and at most 199999"); diff --git a/HeavenClient/tests/shot_path.cpp b/HeavenClient/tests/shot_path.cpp index e3e8cdbd3..f94b61097 100644 --- a/HeavenClient/tests/shot_path.cpp +++ b/HeavenClient/tests/shot_path.cpp @@ -35,6 +35,14 @@ int main() require(hit_delay(left, { -150, 172 }) == 277, "a hit lands 1.5 ms per pixel across"); require(hit_delay(left, { -150, 100 }) == 277, "only the horizontal distance times the hit"); + require(hops(5121002) && hops(15111006) && !hops(5001003), "Energy Orb's and Spark's balls jump"); + require(hop_start({ 100, 50 }, false) == ms::Point(125, 50) && + hop_start({ 100, 50 }, true) == ms::Point(75, 50), + "from 25 px ahead"); + require(hop_point({ -50, 50, 0, 101 }) == ms::Point(0, 50), "to the middle of a body"); + require(hop_time(5121002, { 0, 0 }, { 300, 400 }) == 625, "an orb jumps 1.25 ms per pixel"); + require(hop_time(15111006, { 0, 0 }, { 300, 400 }) == 1000, "a spark 2 ms"); + require(spread(0, 1) == 0, "a single bullet flies at its target"); require(spread(0, 2) == -7 && spread(1, 2) == 7, "two bullets fly 7 px either side"); require(spread(0, 3) == -14 && spread(2, 3) == 14, "three bullets fly 14 px apart");