From 12e819bbd229050767305b38e7c8b2bde9281e6b Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 01:14:40 -0400 Subject: [PATCH] c25 Hypnotize client: MobStat bit 28 (Dazzle) named; a dazzled mob's touch and attacks hit no player (0x6663d2, 0x95825c); Hypnotize's and Heaven's Hammer's hits show no number or miss, whoever hits (0x5c9e7d) --- HeavenClient/Gameplay/Combat/Combat.cpp | 8 +++++--- HeavenClient/Gameplay/Combat/DamageLayout.h | 8 ++++++++ HeavenClient/Gameplay/MapleMap/Mob.cpp | 16 ++++++++++++---- HeavenClient/Gameplay/MapleMap/Mob.h | 2 ++ .../Net/Handlers/Helpers/MobStatParser.h | 2 ++ HeavenClient/tests/damage_layout.cpp | 4 ++++ HeavenClient/tests/mob_stat_parser.cpp | 13 +++++++++++++ 7 files changed, 46 insertions(+), 7 deletions(-) diff --git a/HeavenClient/Gameplay/Combat/Combat.cpp b/HeavenClient/Gameplay/Combat/Combat.cpp index 377540dda..2b40bfde3 100644 --- a/HeavenClient/Gameplay/Combat/Combat.cpp +++ b/HeavenClient/Gameplay/Combat/Combat.cpp @@ -1933,9 +1933,11 @@ namespace ms void Combat::apply_damage_effect(const DamageEffect &effect) { - Point head_position = mobs.get_mob_head_position(effect.target_oid); - damagenumbers.push_back(effect.number); - damagenumbers.back().set_anchor(head_position); + if (damage_layout::shows_number(effect.move_id)) + { + damagenumbers.push_back(effect.number); + damagenumbers.back().set_anchor(mobs.get_mob_head_position(effect.target_oid)); + } const SpecialMove &move = get_move(effect.move_id); mobs.apply_damage(effect.target_oid, effect.damage, effect.toleft, effect.user, move); diff --git a/HeavenClient/Gameplay/Combat/DamageLayout.h b/HeavenClient/Gameplay/Combat/DamageLayout.h index e6b354672..8d61ebc09 100644 --- a/HeavenClient/Gameplay/Combat/DamageLayout.h +++ b/HeavenClient/Gameplay/Combat/DamageLayout.h @@ -95,5 +95,13 @@ namespace ms { return 15 + line * (close ? 15 : 30); } + + // Whether a hit by `skill` shows a number or a miss on the mob: + // Heaven's Hammer's and Hypnotize's show neither, whoever hits + // (0x5c9e7d). + inline bool shows_number(int32_t skill) + { + return skill != 1221011 && skill != 5221009; + } } } diff --git a/HeavenClient/Gameplay/MapleMap/Mob.cpp b/HeavenClient/Gameplay/MapleMap/Mob.cpp index 39725d7b9..06006404c 100644 --- a/HeavenClient/Gameplay/MapleMap/Mob.cpp +++ b/HeavenClient/Gameplay/MapleMap/Mob.cpp @@ -1002,7 +1002,10 @@ namespace ms } // A box hits the player where they are once due; a jump attack only - // one standing on a foothold. A direct shot hits regardless. + // one standing on a foothold. A direct shot hits regardless. A + // dazzled mob's attacks hit no player (0x6663d2, 0x95825c). + bool strikes = !targetdead && !dazzled(); + for (auto iter = pendinghits.begin(); iter != pendinghits.end();) { if (clock < iter->due) @@ -1015,7 +1018,7 @@ namespace ms bool hit = !iter->box || (mob_attack::intersects(*iter->box, targetbody) && (info.type != 0 || !info.jump || targetonground)); - if (hit && !targetdead) + if (hit && strikes) landed.push_back({ make_attack(iter->index, iter->left), attacks[iter->index].hit }); iter = pendinghits.erase(iter); @@ -1039,7 +1042,7 @@ namespace ms int32_t bx = static_cast(iter->from.x() + (iter->to.x() - iter->from.x()) * progress); int32_t by = static_cast(iter->from.y() + (iter->to.y() - iter->from.y()) * progress); - if (mob_attack::contains(targetbody, bx, by) && !targetdead) + if (mob_attack::contains(targetbody, bx, by) && strikes) landed.push_back({ make_attack(iter->index, iter->left), attacks[iter->index].hit }); } @@ -2051,6 +2054,11 @@ namespace ms statuses.count(MobStatParser::WEB); } + bool Mob::dazzled() const + { + return statuses.count(MobStatParser::DAZZLE); + } + void Mob::show_status(int32_t reason, bool effects) { // A player's skill, or a mob skill's id and level (0x5d3d02). @@ -2525,7 +2533,7 @@ namespace ms bool Mob::deals_touch_damage() const { - return touchdamage && !notattack; + return touchdamage && !notattack && !dazzled(); } bool Mob::is_in_range(const Rectangle &range) const diff --git a/HeavenClient/Gameplay/MapleMap/Mob.h b/HeavenClient/Gameplay/MapleMap/Mob.h index fe4141576..23ed2231f 100644 --- a/HeavenClient/Gameplay/MapleMap/Mob.h +++ b/HeavenClient/Gameplay/MapleMap/Mob.h @@ -344,6 +344,8 @@ namespace ms // Whether a status holds the mob where it stands. bool held() const; + // Whether Hypnotize keeps the mob from hurting players. + bool dazzled() const; // Show a status's skill on the mob: its `mob` effect once, or for // as long as the status lasts if it repeats, and its `mob0` for as diff --git a/HeavenClient/Net/Handlers/Helpers/MobStatParser.h b/HeavenClient/Net/Handlers/Helpers/MobStatParser.h index d8b6f22c3..026bcd2a4 100644 --- a/HeavenClient/Net/Handlers/Helpers/MobStatParser.h +++ b/HeavenClient/Net/Handlers/Helpers/MobStatParser.h @@ -55,6 +55,8 @@ namespace ms VENOM = 24, BLIND = 25, BURNED = 27, + // Hypnotize's: the mob hurts no player (0x6663d2, 0x95825c). + DAZZLE = 28, PCOUNTER = 29, MCOUNTER = 30, DISABLE = 31, diff --git a/HeavenClient/tests/damage_layout.cpp b/HeavenClient/tests/damage_layout.cpp index 9394d507e..32ede11c7 100644 --- a/HeavenClient/tests/damage_layout.cpp +++ b/HeavenClient/tests/damage_layout.cpp @@ -29,6 +29,10 @@ int main() require(width == 96 && placed[0].left == 30, "a critical number starts right of its banner"); require(placed[0].top == 15 && placed[1].top == 22, "drops apply to odd digits too"); + require(damage_layout::shows_number(5221004), "a hit shows its number"); + require(!damage_layout::shows_number(5221009) && !damage_layout::shows_number(1221011), + "Hypnotize's and Heaven's Hammer's don't"); + require(damage_layout::left(66) == -33 && damage_layout::left(67) == -33, "the canvas is centred, rounding left"); require(damage_layout::opacity(0) == 1.0f && damage_layout::opacity(400) == 1.0f, "opaque for 400 ms"); diff --git a/HeavenClient/tests/mob_stat_parser.cpp b/HeavenClient/tests/mob_stat_parser.cpp index cf30a712a..3f11bc3ca 100644 --- a/HeavenClient/tests/mob_stat_parser.cpp +++ b/HeavenClient/tests/mob_stat_parser.cpp @@ -85,6 +85,19 @@ int main() require(reset.read_byte() == 9, "A reset reads past the burns' ids"); require(!MobStatParser::stops(burned), "Burns don't stop the mob"); + // Hypnotize's Dazzle (28) is a plain status after the burns. + BuffParser::Flags dazzle{ 0, 0 }; + dazzle.set(MobStatParser::DAZZLE); + Writer d; + d.status(1, 5221009, 21).put(int8_t(9)); + + InPacket dazzled(d.bytes.data(), d.bytes.size()); + std::vector hypnotized = MobStatParser::parse(dazzled, dazzle); + require(hypnotized.size() == 1 && hypnotized[0].stat == 28 && hypnotized[0].reason == 5221009 && + hypnotized[0].duration == 21, "Dazzle is read as bit 28"); + require(dazzled.read_byte() == 9, "And nothing more"); + require(!MobStatParser::stops(dazzle), "Dazzle doesn't stop the mob"); + std::cout << "mob stat parser tests passed\n"; return 0; }