mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
PvP & HUD: 1.8 knockback under legacy combat rules
With the legacy rules on, a player's knockback follows 1.8.9's EntityLivingBase.knockBack: the motion halves and the 0.4 push lifts in the air too (26.3 lifts only from the ground), and knockback resistance is a chance to take none instead of a cut. The sprint and Knockback push is added on top as attackTargetEntityWithCurrentItem's addVelocity (0.1 up) instead of going through knockback, for players and mobs alike. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f06b417ac4
commit
3400a0316b
6 files changed
+101
-6
No files matched your search
@@ -12315,7 +12315,19 @@ impl Game<'_> {
|
||||
if landed {
|
||||
if knockback > 0.0 {
|
||||
let (xd, zd) = self.extra_knockback_direction();
|
||||
self.mobs[index].knockback_effect(f64::from(knockback), xd, zd, &source, amount);
|
||||
if self.legacy_rules() {
|
||||
let m = &mut self.mobs[index];
|
||||
let v = [m.velocity.x, m.velocity.y, m.velocity.z];
|
||||
let [x, y, z] = horizon_client::legacy_combat::extra_knockback(
|
||||
v,
|
||||
f64::from(knockback),
|
||||
(xd, zd),
|
||||
);
|
||||
m.velocity = Vec3::new(x, y, z);
|
||||
m.needs_sync = true;
|
||||
} else {
|
||||
self.mobs[index].knockback_effect(f64::from(knockback), xd, zd, &source, amount);
|
||||
}
|
||||
self.stop_sprint_after_extra_knockback();
|
||||
}
|
||||
let mob = &self.mobs[index];
|
||||
|
||||
@@ -3856,8 +3856,9 @@ impl Game<'_> {
|
||||
0 => credit,
|
||||
attacker => attacker,
|
||||
});
|
||||
let legacy = self.legacy_rules();
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
knock_back(p, &mut self.world.random, power, (xd, zd), source);
|
||||
knock_back(p, &mut self.world.random, power, (xd, zd), source, legacy);
|
||||
}
|
||||
}
|
||||
/// A status effect from a mob, arrow or splash potion (`addEffect`).
|
||||
@@ -7824,13 +7825,15 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `LivingEntity.knockback` on `p`: [`Game::knock_back_player`].
|
||||
/// `LivingEntity.knockback` on `p`: [`Game::knock_back_player`]; 1.8.9's
|
||||
/// `EntityLivingBase.knockBack` under `legacy` rules.
|
||||
fn knock_back(
|
||||
p: &mut horizon_client::motion::PlayerMotion,
|
||||
random: &mut horizon_core::random::LegacyRandom,
|
||||
power: f64,
|
||||
(xd, zd): (f64, f64),
|
||||
source: HurtSource<'_>,
|
||||
legacy: bool,
|
||||
) {
|
||||
let resistance = p.attribute("minecraft:knockback_resistance").unwrap_or(0.0);
|
||||
// The native hook runs first, on the raw strength (`LivingKnockBackEvent`), then the
|
||||
@@ -7848,7 +7851,14 @@ fn knock_back(
|
||||
x: mut xd,
|
||||
z: mut zd,
|
||||
} = knock;
|
||||
let power = power * (1.0 - resistance);
|
||||
if legacy && horizon_client::legacy_combat::resists(resistance, random.next_double()) {
|
||||
return;
|
||||
}
|
||||
let power = if legacy {
|
||||
power
|
||||
} else {
|
||||
power * (1.0 - resistance)
|
||||
};
|
||||
if power <= 0.0 {
|
||||
return;
|
||||
}
|
||||
@@ -7856,6 +7866,12 @@ fn knock_back(
|
||||
xd = (random.next_double() - random.next_double()) * 0.01;
|
||||
zd = (random.next_double() - random.next_double()) * 0.01;
|
||||
}
|
||||
if legacy {
|
||||
let v = p.velocity;
|
||||
let [x, y, z] = horizon_client::legacy_combat::knockback([v.x, v.y, v.z], power, (xd, zd));
|
||||
p.velocity = Vec3::new(x, y, z);
|
||||
return;
|
||||
}
|
||||
let l = (xd * xd + zd * zd).sqrt();
|
||||
let v = p.velocity;
|
||||
p.velocity = Vec3::new(
|
||||
@@ -8357,7 +8373,7 @@ mod knockback_tests {
|
||||
let set = p.set_modifier(attribute, "horizon:test", resistance, "add_value", true);
|
||||
set.unwrap();
|
||||
}
|
||||
knock_back(&mut p, &mut random, 0.4, (0.0, 3.0), hit);
|
||||
knock_back(&mut p, &mut random, 0.4, (0.0, 3.0), hit, false);
|
||||
(p.velocity.x, p.velocity.y, p.velocity.z)
|
||||
};
|
||||
// No mod in the way: vanilla's push, away from z = 3.
|
||||
|
||||
@@ -325,7 +325,11 @@ impl Game<'_> {
|
||||
g.combat.player_attacker = None;
|
||||
let (strength, xd, zd) = s.knockback;
|
||||
if hurt && strength > 0.0 {
|
||||
g.knock_back_player(f64::from(strength), xd, zd, PVP_SOURCE);
|
||||
if g.legacy_rules() {
|
||||
g.legacy_extra_knockback(f64::from(strength), (xd, zd));
|
||||
} else {
|
||||
g.knock_back_player(f64::from(strength), xd, zd, PVP_SOURCE);
|
||||
}
|
||||
}
|
||||
// Entity.igniteForSeconds on the victim: the fire timer only
|
||||
// rises, and a fire-immune victim (no player is by type) is spared.
|
||||
|
||||
@@ -57,6 +57,17 @@ impl Game<'_> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The sprint and Knockback push on this game's player (a guest swapped in), added as 1.8.9 did.
|
||||
pub(super) fn legacy_extra_knockback(&mut self, strength: f64, dir: (f64, f64)) {
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
let v = p.velocity;
|
||||
let [x, y, z] = legacy_combat::extra_knockback([v.x, v.y, v.z], strength, dir);
|
||||
p.velocity.x = x;
|
||||
p.velocity.y = y;
|
||||
p.velocity.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_attacker(&self) -> legacy_combat::Attacker {
|
||||
let p = self.player.as_ref();
|
||||
legacy_combat::Attacker {
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
//! operation 2 products, clamped to the attribute's minimum 0.
|
||||
//! - `EnchantmentDamage.calcDamageByCreature`: Sharpness 1.25 per level, Smite 2.5 per level
|
||||
//! against `UNDEAD`, Bane of Arthropods 2.5 per level against `ARTHROPOD`.
|
||||
//! - `EntityLivingBase.knockBack`: resisted only by chance (`rand.nextDouble() < knockbackResistance`); otherwise
|
||||
//! the motion halves, the push of 0.4 goes away from the attacker, and up by 0.4 (at most 0.4 in all), in the air
|
||||
//! too (26.3 lifts only from the ground and scales the push by the resistance).
|
||||
//! - The sprint and Knockback push in `attackTargetEntityWithCurrentItem`: `addVelocity(-sin(yaw) * i * 0.5, 0.1,
|
||||
//! cos(yaw) * i * 0.5)`, added to the motion as it is (26.3 halves it first, through `knockback`).
|
||||
use std::sync::Arc;
|
||||
|
||||
/// `ToolMaterial.getDamageVsEntity` by the item id's material. Netherite and copper didn't
|
||||
@@ -136,6 +141,30 @@ pub fn damage(attribute: f32, crit: Option<f32>, enchant: f32) -> f32 {
|
||||
base + enchant
|
||||
}
|
||||
|
||||
/// `EntityLivingBase.knockBack` on `velocity`: `power` away from the direction (`xd`, `zd`), which points at the
|
||||
/// attacker, and up, in the air as on the ground. The caller has already rolled the resistance.
|
||||
pub fn knockback(velocity: [f64; 3], power: f64, (xd, zd): (f64, f64)) -> [f64; 3] {
|
||||
let len = (xd * xd + zd * zd).sqrt();
|
||||
[
|
||||
velocity[0] / 2.0 - xd / len * power,
|
||||
(velocity[1] / 2.0 + power).min(0.4),
|
||||
velocity[2] / 2.0 - zd / len * power,
|
||||
]
|
||||
}
|
||||
/// Whether a knockback resistance of `resistance` stops the push, from a roll of `roll` in [0, 1).
|
||||
pub fn resists(resistance: f64, roll: f64) -> bool {
|
||||
roll < resistance
|
||||
}
|
||||
/// The extra push of a sprinting or Knockback hit, `strength` along (`xd`, `zd`) (the attacker's look, as
|
||||
/// `extra_knockback_direction` gives it: 26.3's `knockback` pushes away from it), and 0.1 up.
|
||||
pub fn extra_knockback(velocity: [f64; 3], strength: f64, (xd, zd): (f64, f64)) -> [f64; 3] {
|
||||
[
|
||||
velocity[0] - xd * strength,
|
||||
velocity[1] + 0.1,
|
||||
velocity[2] - zd * strength,
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -226,4 +255,24 @@ mod tests {
|
||||
assert_eq!(damage(0.0, Some(CRIT), 1.25), 1.25);
|
||||
assert_eq!(damage(8.0, None, 0.0), 8.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hit_lifts_a_player_in_the_air_too() {
|
||||
// Hit from z = +1 while falling at 0.5 a tick: 1.8.9 halves the fall and adds 0.4 (0.15 up);
|
||||
// 26.3 would leave the fall as it was.
|
||||
let v = knockback([0.0, -0.5, 0.0], 0.4, (0.0, 1.0));
|
||||
assert_eq!(v, [0.0, 0.15000000000000002, -0.4]);
|
||||
// Already rising fast: capped at 0.4.
|
||||
assert_eq!(knockback([0.2, 0.6, 0.0], 0.4, (3.0, 4.0))[1], 0.4);
|
||||
assert!((knockback([0.2, 0.6, 0.0], 0.4, (3.0, 4.0))[0] - (0.1 - 0.24)).abs() < 1e-12);
|
||||
// Resistance is a chance, not a cut: 0.6 stops 60% of the rolls.
|
||||
assert!(resists(0.6, 0.59) && !resists(0.6, 0.6) && !resists(0.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sprint_hit_adds_its_push_without_halving() {
|
||||
// A sprint hit (i = 1) looking along +x: (sin, -cos) of yaw -90 is (-1, 0); the target flies to +x.
|
||||
let v = extra_knockback([0.1, 0.0, 0.0], 0.5, (-1.0, 0.0));
|
||||
assert_eq!(v, [0.6, 0.1, 0.0]);
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,9 @@ own rules. Melee hits follow 1.8.9's `EntityPlayer.attackTargetEntityWithCurrent
|
||||
- Strength multiplies by 1 + 1.3 per level, Weakness subtracts 0.5 per level;
|
||||
- Sharpness adds 1.25 per level, Smite and Bane of Arthropods 2.5 per level against their targets;
|
||||
- a critical hit (×1.5) needs a fall, not a charge, and can be a sprinting hit.
|
||||
- knockback on players as `EntityLivingBase.knockBack`: it lifts a player in the air too (combos), and knockback
|
||||
resistance is a chance to take none instead of a cut; the sprint and Knockback push is added on top (0.1 up)
|
||||
instead of halving the motion first.
|
||||
|
||||
Code: `crates/horizon-client/src/legacy_combat.rs` (the formulas, with their 1.8.9 sources),
|
||||
`game/legacy_rules.rs`; `lan_e2e.py --check legacy-pvp` measures it between two games.
|
||||
|
||||
Reference in new issue
Block a user