From 5a0b13800a8f3d62ace8ce3faca995f8f27a91c4 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 01:55:11 -0400 Subject: [PATCH] c29 Disorder and Hypnotize hits give no aggro, and in HC end the mob's chase (c021: 0x7a82a2 passes 0 to FUN_007f73d8) --- HeavenClient/Gameplay/Combat/Attack.h | 3 +++ HeavenClient/Gameplay/Combat/Combat.cpp | 3 ++- HeavenClient/Gameplay/Combat/DamageLayout.h | 9 +++++++++ HeavenClient/Gameplay/MapleMap/MapMobs.cpp | 3 +++ HeavenClient/Gameplay/MapleMap/Mob.cpp | 9 +++++++++ HeavenClient/Gameplay/MapleMap/Mob.h | 4 ++++ HeavenClient/tests/damage_layout.cpp | 4 ++++ agent_docs/combat.md | 6 +++++- integration-harness/tests/hypnotize.rs | 11 ++++++++--- net/src/skills.rs | 20 ++++++++++++++++++++ runtime/src/actor/field.rs | 4 +++- 11 files changed, 70 insertions(+), 6 deletions(-) diff --git a/HeavenClient/Gameplay/Combat/Attack.h b/HeavenClient/Gameplay/Combat/Attack.h index b90b655b..ec7534f8 100644 --- a/HeavenClient/Gameplay/Combat/Attack.h +++ b/HeavenClient/Gameplay/Combat/Attack.h @@ -242,5 +242,8 @@ namespace ms Rectangle range; // The element of the charge on the weapon, or 0. uint8_t charge = 0; + // Whether the hit ends the mob's chase: the local player's Disorder + // or Hypnotize. + bool calms = false; }; } \ No newline at end of file diff --git a/HeavenClient/Gameplay/Combat/Combat.cpp b/HeavenClient/Gameplay/Combat/Combat.cpp index 2b40bfde..109ca3c7 100644 --- a/HeavenClient/Gameplay/Combat/Combat.cpp +++ b/HeavenClient/Gameplay/Combat/Combat.cpp @@ -2132,7 +2132,8 @@ namespace ms user.get_look().get_equips().get_weapon(), result.stance, range, - physical_damage::charge_element(user.get_charge()) + physical_damage::charge_element(user.get_charge()), + &user == &player && !damage_layout::provokes(move.get_id()) }; bool chain = chain_lightning::chains(result.skill); diff --git a/HeavenClient/Gameplay/Combat/DamageLayout.h b/HeavenClient/Gameplay/Combat/DamageLayout.h index 8d61ebc0..668d67da 100644 --- a/HeavenClient/Gameplay/Combat/DamageLayout.h +++ b/HeavenClient/Gameplay/Combat/DamageLayout.h @@ -103,5 +103,14 @@ namespace ms { return skill != 1221011 && skill != 5221009; } + + // Whether the local player's hit by `skill` (0: a plain attack) + // leaves the mob it hits chasing them as before. Disorder's and + // Hypnotize's don't, and the mob gives up any chase (0x7a82a2 passes + // 0 to FUN_007f73d8). + inline bool provokes(int32_t skill) + { + return skill != 4001002 && skill != 14001002 && skill != 5221009; + } } } diff --git a/HeavenClient/Gameplay/MapleMap/MapMobs.cpp b/HeavenClient/Gameplay/MapleMap/MapMobs.cpp index 799dd1e2..9fee1876 100644 --- a/HeavenClient/Gameplay/MapleMap/MapMobs.cpp +++ b/HeavenClient/Gameplay/MapleMap/MapMobs.cpp @@ -185,6 +185,9 @@ namespace ms { mob->apply_damage(damage, toleft); + if (user.calms) + mob->calm(); + // TODO: Maybe move this into the method above too? move.apply_hiteffects(user, *mob); } diff --git a/HeavenClient/Gameplay/MapleMap/Mob.cpp b/HeavenClient/Gameplay/MapleMap/Mob.cpp index 06006404..443466de 100644 --- a/HeavenClient/Gameplay/MapleMap/Mob.cpp +++ b/HeavenClient/Gameplay/MapleMap/Mob.cpp @@ -1705,6 +1705,15 @@ namespace ms update_shoe(true); } + void Mob::calm() + { + if (!control) + return; + + hastarget = false; + update_shoe(false); + } + void Mob::update_shoe(bool chasing) { // v83 mobs walk on a CAttrShoe (FUN_005c55cd): its speed is diff --git a/HeavenClient/Gameplay/MapleMap/Mob.h b/HeavenClient/Gameplay/MapleMap/Mob.h index 23ed2231..b20d0efc 100644 --- a/HeavenClient/Gameplay/MapleMap/Mob.h +++ b/HeavenClient/Gameplay/MapleMap/Mob.h @@ -200,6 +200,10 @@ namespace ms // Apply damage to the mob. void apply_damage(int32_t damage, bool toleft); + // End a controlled mob's chase, as a local Disorder or Hypnotize hit + // does (FUN_007f73d8 with 0). + void calm(); + // Monster Magnet caught the mob for the user at `user`: if this // client moves it, it flinches toward them, is pulled in, and // chases them (0x66b6fc, 0x66b562). diff --git a/HeavenClient/tests/damage_layout.cpp b/HeavenClient/tests/damage_layout.cpp index 32ede11c..ec10aa30 100644 --- a/HeavenClient/tests/damage_layout.cpp +++ b/HeavenClient/tests/damage_layout.cpp @@ -33,6 +33,10 @@ int main() require(!damage_layout::shows_number(5221009) && !damage_layout::shows_number(1221011), "Hypnotize's and Heaven's Hammer's don't"); + require(damage_layout::provokes(0) && damage_layout::provokes(4001334), "a hit leaves the chase alone"); + require(!damage_layout::provokes(4001002) && !damage_layout::provokes(14001002) + && !damage_layout::provokes(5221009), "Disorder's and Hypnotize's end it"); + 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/agent_docs/combat.md b/agent_docs/combat.md index 288de3b9..475c16cd 100644 --- a/agent_docs/combat.md +++ b/agent_docs/combat.md @@ -80,7 +80,11 @@ templates from `Mob.nx` (`maxHP`, `exp`, `PADamage`, `bodyAttack`, - **Aggro:** a hit hands the mob to the attacker with aggro (SPAWN_MONSTER_CONTROL 2) for `AGGRO_DURATION` (180 s, v83's minimum chase time). The previous controller gets SPAWN_MONSTER_CONTROL 0, and later hits - from the same player within that time send nothing. + from the same player within that time send nothing. Hits by Disorder + (4001002, 14001002) and Hypnotize (5221009) give no aggro + (`skills::provokes`): v83's client has the mob they hit give up its chase + instead (0x7a82a2 passes 0 to FUN_007f73d8), as HeavenClient's `Mob::calm` + does for the local player's hits. - **Client AI:** HeavenClient decides each mob's movement in v83's 90 ms steps. Each decision holds for a random time: 1 to 3 s when idle (walk left, right, or stand), and 0.8 to 1.2 s when chasing. A mob given aggro diff --git a/integration-harness/tests/hypnotize.rs b/integration-harness/tests/hypnotize.rs index d5227e0d..22dbbb74 100644 --- a/integration-harness/tests/hypnotize.rs +++ b/integration-harness/tests/hypnotize.rs @@ -1,11 +1,12 @@ //! Hypnotize: a Corsair's close gun shot with no `damage` of its own hits //! for the plain blow and dazzles the mob (MobStat bit 28), at level 20 -//! always. A shot that misses puts nothing on it. +//! always. A shot that misses puts nothing on it. Unlike a plain blow, its +//! hit doesn't give the shooter aggro over the mob. use integration_harness::database::{grant_skill, replace_item, set_job}; -use integration_harness::hunting::skill_attack; +use integration_harness::hunting::{attack, skill_attack}; use integration_harness::preconditions::load_harness_config_or_fail; -use integration_harness::trace::{step, until, warp, Trace}; +use integration_harness::trace::{send, step, until, warp, Trace}; use integration_harness::{login_to_world_session, relogin}; use tokio::time::Duration; @@ -47,4 +48,8 @@ async fn hypnotize_dazzles_the_mob_it_hits() { let t = step(c, "shot", skill_attack(HYPNOTIZE, snails[0], 1)).await; assert!(!t.stat_updates.is_empty(), "the shot is taken"); assert_eq!(t.field.stat_sets, [(snails[0], DAZZLE, 1, HYPNOTIZE)]); + assert!(t.field.aggro.is_empty(), "the shot draws no aggro"); + + send(c, attack(snails[0], 1)).await; + until(c, &mut h, |t| t.field.aggro.contains(&snails[0])).await; } diff --git a/net/src/skills.rs b/net/src/skills.rs index 2631bcd2..c1b01565 100644 --- a/net/src/skills.rs +++ b/net/src/skills.rs @@ -773,6 +773,18 @@ fn hit_statuses(skill: &Learned) -> Option> { /// The Corsair's Hypnotize, a close gun shot that puts Dazzle on the mob. pub const HYPNOTIZE: i32 = 5221009; +/// Disorder (the Rogue's and the Night Walker's) and Hypnotize: a v83 +/// client that hits a mob with them doesn't have it chase the player, and +/// drops any chase it had (0x7a82a2 passes 0 to FUN_007f73d8), so they +/// don't give aggro over the mob either. +const CALMING: [i32; 3] = [4001002, 14001002, HYPNOTIZE]; + +/// Whether a hit with `skill` (None: a plain attack) gives the attacker +/// aggro over the mob. +pub fn provokes(skill: Option) -> bool { + !skill.is_some_and(|skill| CALMING.contains(&skill)) +} + /// Elemental Boost, which adds its `damage` percent to Flamethrower's and /// Ice Splitter's (0x79004c) and, as its description says, its `y` seconds /// to Ice Splitter's freeze. @@ -1588,6 +1600,14 @@ mod tests { ); } + #[test] + fn disorder_and_hypnotize_give_no_aggro() { + assert!(provokes(None) && provokes(Some(4001334)) && provokes(Some(5221004))); + assert!( + !provokes(Some(4001002)) && !provokes(Some(14001002)) && !provokes(Some(HYPNOTIZE)) + ); + } + #[test] fn attacks_put_their_skills_statuses_and_venom_on_what_they_hit() { // Shout at 30: a stun with a 95% chance, for 10 seconds. diff --git a/runtime/src/actor/field.rs b/runtime/src/actor/field.rs index 9eed461d..f63f82e4 100644 --- a/runtime/src/actor/field.rs +++ b/runtime/src/actor/field.rs @@ -1135,7 +1135,9 @@ impl FieldActor { if let Some(eater) = eater { mp_taken += self.mobs.eat_mp(object_id, eater.x); } - self.provoke(from, &attacker, object_id, now); + if net::skills::provokes(skill) { + self.provoke(from, &attacker, object_id, now); + } // A miss puts nothing on it. for inflict in inflicts.iter().filter(|_| damage > 0) { if inflict.takes(rand::random()) {