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
9 changes: 7 additions & 2 deletions agent_docs/combat.md
Original file line number Diff line number Diff line change
Expand Up @@ -943,15 +943,20 @@ carry the statuses a mob has. Entries come in the client's decode order
Dispel take mob buffs off instead.
- An attack's hits carry `skills::on_hit`: the attack skill's own status
(stuns, freezes, poisons, Disorder's lower attack and defence, Taunt's
higher defence) and a learned Venom's poison. Each mob hit for more than
higher defence, Hypnotize's Dazzle) and a learned Venom's poison. Each mob hit for more than
0 rolls each. Elemental Boost makes Ice Splitter's freeze last its `y`
seconds longer; the extra poison its description promises for
Flamethrower is not modelled.
- Taunt also puts on Showdown (bit 11), which no v83 client reads: a mob
that dies with it gives its value in percent more EXP and makes each of
its drops that much likelier (`Kill::showdown`, `loot::roll`; the
OdinMS-family rule).
- Bosses shrug off Stun, Freeze, Seal, Doom and Web. A mob immune to poison
- Hypnotize (5221009) has no `damage` and hits for the plain blow, like
Disorder and Taunt. Its Dazzle (bit 28) stops the mob attacking players
on the client. In v83 other mobs may then hit it, reported by their
controller as MOB_DAMAGE_MOB (0xC2); the server has no handler for that
yet.
- Bosses shrug off Stun, Freeze, Seal, Doom, Web and Dazzle. A mob immune to poison
(`elemAttr` S1) or ice (I1) shrugs off Poison or Freeze.
- Poison, Venom and Ambush take HP each second on the field tick
(`MobSet::hurt_by_statuses`), the mob's `fixedDamage` instead if it has
Expand Down
7 changes: 6 additions & 1 deletion game/src/mob_stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,12 @@ impl MobStat {
pub fn spares_bosses(self) -> bool {
matches!(
self,
MobStat::Stun | MobStat::Freeze | MobStat::Seal | MobStat::Doom | MobStat::Web
MobStat::Stun
| MobStat::Freeze
| MobStat::Seal
| MobStat::Doom
| MobStat::Web
| MobStat::Dazzle
)
}
}
50 changes: 50 additions & 0 deletions integration-harness/tests/hypnotize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! 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.

use integration_harness::database::{grant_skill, replace_item, set_job};
use integration_harness::hunting::skill_attack;
use integration_harness::preconditions::load_harness_config_or_fail;
use integration_harness::trace::{step, until, warp, Trace};
use integration_harness::{login_to_world_session, relogin};
use tokio::time::Duration;

const HUNTING_GROUND: i32 = 40_001;
const SNAIL: i32 = 100100;
const CORSAIR: i16 = 522;
const HYPNOTIZE: i32 = 5221009;
const GUN: i32 = 1492000;
const EQUIPPED: i16 = -1;
const WEAPON: i16 = -11;
const DAZZLE: u128 = 1 << 28;

#[tokio::test]
async fn hypnotize_dazzles_the_mob_it_hits() {
let config = load_harness_config_or_fail().await;
let id = login_to_world_session(&config)
.await
.expect("first login failed")
.character_id;
drop(relogin(&config).await);
tokio::time::sleep(Duration::from_millis(500)).await;
set_job(id, CORSAIR, 120, 1000).await.expect("Corsair");
grant_skill(id, HYPNOTIZE, 20).await.expect("Hypnotize");
replace_item(id, EQUIPPED, WEAPON, GUN)
.await
.expect("give a gun");

let mut session = relogin(&config).await;
let c = &mut session.connection;
let mut h = Trace::default();
assert_eq!(warp(c, HUNTING_GROUND, "", &mut h).await, HUNTING_GROUND);
until(c, &mut h, |t| t.field.alive(SNAIL).len() >= 2).await;
let snails = h.field.alive(SNAIL);

let t = step(c, "miss", skill_attack(HYPNOTIZE, snails[1], 0)).await;
assert!(!t.stat_updates.is_empty(), "the shot is taken");
assert!(t.field.stat_sets.is_empty(), "a miss dazzles nothing");

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)]);
}
15 changes: 11 additions & 4 deletions net/src/combat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::charge;
use crate::combo;
use crate::energy;
use crate::meso_explosion::{self, Blast, Explosion, MESO_EXPLOSION};
use crate::skills::{self, Learned};
use crate::skills::{self, Learned, HYPNOTIZE};
use crate::{error::NetworkError, packet::op::SendOpcode};
use game::inventory::equip_slot;
use game::{BuffStat, InventoryType, Player};
Expand Down Expand Up @@ -790,7 +790,9 @@ fn sacrifice_cost(attack: &Attack, x: i32, hp: i16) -> i32 {
fn damage_percent(skill: &Learned) -> Option<i32> {
match (skill.effect.damage, skill.skill_id) {
(0, ARROW_BOMB) => Some(skill.effect.x),
(0, 4001002 | 14001002 | 4121003 | 4221003) => Some(100),
// Disorder, Taunt, and Hypnotize have no `damage`: the client hits
// for the plain blow.
(0, 4001002 | 14001002 | 4121003 | 4221003 | HYPNOTIZE) => Some(100),
(0, _) => None,
(damage, _) => Some(damage),
}
Expand Down Expand Up @@ -1854,7 +1856,7 @@ mod tests {
}

#[test]
fn disorder_and_taunt_hit_for_the_base_cap() {
fn disorder_taunt_and_hypnotize_hit_for_the_base_cap() {
if skills::data().is_err() {
return;
}
Expand All @@ -1865,8 +1867,13 @@ mod tests {
player.stats.mp = 100;
learn(&mut player, 4001002, 20);
learn(&mut player, 4121003, 30);
learn(&mut player, HYPNOTIZE, 20);
let cap = (max_base_damage(&player, &hit(AttackKind::Close, 4001002, 0)) * 2).max(10);
for (kind, skill) in [(AttackKind::Close, 4001002), (AttackKind::Ranged, 4121003)] {
for (kind, skill) in [
(AttackKind::Close, 4001002),
(AttackKind::Ranged, 4121003),
(AttackKind::Close, HYPNOTIZE),
] {
assert!(validate(&hit(kind, skill, cap), &player).is_ok());
assert_eq!(
validate(&hit(kind, skill, cap + 1), &player).unwrap_err(),
Expand Down
21 changes: 21 additions & 0 deletions net/src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,11 +760,17 @@ fn hit_statuses(skill: &Learned) -> Option<Vec<(MobStat, Amount)>> {
value(MobStat::Pdr, effect.x),
value(MobStat::Mdr, effect.x),
],
// Hypnotize: the mob stops attacking players and other mobs may hit
// it (MobStat bit 28, 0x6663d2), by its `prop` for its `time`.
HYPNOTIZE => one(MobStat::Dazzle),
_ => return None,
};
Some(statuses)
}

/// The Corsair's Hypnotize, a close gun shot that puts Dazzle on the mob.
pub const HYPNOTIZE: i32 = 5221009;

/// 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 @@ -1661,6 +1667,21 @@ mod tests {
);
}

#[test]
fn hypnotize_dazzles_by_its_prop_for_its_time() {
let player = knowing(&[]);
let hypnotize = at_level(HYPNOTIZE, 1).unwrap();
let [inflict] = &on_hit(&player, Some(&hypnotize), false, 0.5)[..] else {
panic!("one inflict");
};
assert_eq!(inflict.skill_id, HYPNOTIZE);
assert_eq!(inflict.prop, 43);
assert_eq!(inflict.duration, Duration::from_secs(21));
assert_eq!(inflict.statuses, [(MobStat::Dazzle, Amount::Value(1))]);
assert!(inflict.takes(0.42) && !inflict.takes(0.43));
assert!(MobStat::Dazzle.spares_bosses());
}

#[test]
fn smokescreen_leaves_smoke_and_poison_mist_poison() {
assert_eq!(mist_kind(SMOKESCREEN), MistKind::Smoke);
Expand Down
Loading