From e6f1ae6854dd82b8870f277fe1439221a521bf5e Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Sat, 17 Oct 2026 16:10:29 -0400 Subject: [PATCH 1/3] L140: Energy Orb's ball hops from mob to mob v83 (FUN_0094289f) flies Energy Orb's ball from 25 px in front of the shot's origin to the middle of its first mob, then on to the middle of each next one, 1.25 ms per pixel (Spark 2). Each mob is hit when the ball arrives, so the hits come one hop after another rather than all by horizontal distance. The client now does this for its own shots and other users', and draws no regular bullet for these skills. --- HeavenClient/Gameplay/Combat/Combat.cpp | 35 +++++++++++++++++++++-- HeavenClient/Gameplay/Combat/RemoteShot.h | 11 ++----- HeavenClient/Gameplay/Combat/ShotPath.h | 34 ++++++++++++++++++++++ HeavenClient/tests/shot_path.cpp | 8 ++++++ 4 files changed, 77 insertions(+), 11 deletions(-) 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/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/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"); From c3db309b3a34c2ec5da1523321bb55afb9946635 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Sat, 17 Oct 2026 16:23:46 -0400 Subject: [PATCH 2/3] L139: apply mob statuses and staff element boosts to magic damage The v83 client's MDamage (FUN_00791617) reads the mob's temporary stats on every hit. Disable turns the hit away. Magic Immunity lets only a roll of 0 in 100 through and otherwise shows 1. The EVA and MDR statuses add to the avoid and magic defense. Magic Guard Up leaves n% after the defense. Hard Skin stops every hit that is not critical, and only Evans crit spells. Weakness adds n%. The hit is also scaled by the weapon's percent for the spell's element (FUN_005d284f): the Elemental Wands and Staffs carry elemDefault and incRMAI/F/L/S/H, and every other weapon gives 100. HeavenClient now keeps Disable's byte as a status, names HardSkin and Weakness, reads the element boosts into EquipData, carries them on the Attack, and applies each step in Mob::next_magic_damage in the client's order. --- HeavenClient/Data/EquipData.cpp | 17 ++++++++++++ HeavenClient/Data/EquipData.h | 8 ++++++ HeavenClient/Gameplay/Combat/Attack.h | 2 ++ HeavenClient/Gameplay/Combat/MagicDamage.h | 27 +++++++++++++++++++ HeavenClient/Gameplay/Combat/Skill.cpp | 15 +++++++++++ HeavenClient/Gameplay/MapleMap/Mob.cpp | 20 ++++++++++++-- .../Net/Handlers/Helpers/MobStatParser.cpp | 7 ++++- .../Net/Handlers/Helpers/MobStatParser.h | 8 +++--- HeavenClient/tests/magic_damage.cpp | 11 ++++++++ 9 files changed, 109 insertions(+), 6 deletions(-) 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/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/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..2a497874c 100644 --- a/HeavenClient/Gameplay/MapleMap/Mob.cpp +++ b/HeavenClient/Gameplay/MapleMap/Mob.cpp @@ -2099,10 +2099,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 +2119,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/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"); From 1a1d13a81ebee12b2c20ce3b134c3cc048e0a2af Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Sat, 17 Oct 2026 16:29:02 -0400 Subject: [PATCH 3/3] L174: apply mob statuses to weapon hits CalcDamage::PDamage (FUN_0078df87) gates a blow on the same mob statuses MDamage does: Disable makes it miss, Physical Immune caps it at 1 unless the respect roll pierces it, the EVA and PDR statuses add to the mob's avoid and weapon defense, Power Guard Up scales it after the crit (except for Sacrifice), and Hard Skin stops every hit that is not critical. HeavenClient's weapon damage ignored all of them; it now applies them to weapon hits, while fixed damage still skips them as in v83. Unlike magic, PDamage never reads Weakness: its extra percent is Dragon Knight Berserk's damage. --- HeavenClient/Gameplay/MapleMap/Mob.cpp | 35 +++++++++++++++++++++++--- HeavenClient/Gameplay/MapleMap/Mob.h | 3 +++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/HeavenClient/Gameplay/MapleMap/Mob.cpp b/HeavenClient/Gameplay/MapleMap/Mob.cpp index 2a497874c..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) 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;