Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions HeavenClient/Gameplay/Combat/Attack.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,8 @@ namespace ms
Rectangle<int16_t> 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;
};
}
3 changes: 2 additions & 1 deletion HeavenClient/Gameplay/Combat/Combat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions HeavenClient/Gameplay/Combat/DamageLayout.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
3 changes: 3 additions & 0 deletions HeavenClient/Gameplay/MapleMap/MapMobs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 9 additions & 0 deletions HeavenClient/Gameplay/MapleMap/Mob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions HeavenClient/Gameplay/MapleMap/Mob.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 4 additions & 0 deletions HeavenClient/tests/damage_layout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
6 changes: 5 additions & 1 deletion agent_docs/combat.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions integration-harness/tests/hypnotize.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
}
20 changes: 20 additions & 0 deletions net/src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,18 @@ fn hit_statuses(skill: &Learned) -> Option<Vec<(MobStat, Amount)>> {
/// 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<i32>) -> 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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion runtime/src/actor/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Loading