mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
mobs: a powered wither refuses a player's wind charge
WitherBoss.hurtServer refuses a direct AbstractArrow or WindCharge once isPowered. A wind charge hurts mobs as DamageKind::Thrown, which wither::accepts let through, so every charge took a point off the armor. The check is on the projectile, not the damage kind. A breeze's BreezeWindCharge deals the same damage but is not a WindCharge (both only extend AbstractWindCharge), so it still hurts, unlike what the issue asks. /damage with the wind_charge type has no direct charge and goes through too. tools/test_wither_wind_charge_parity.py compares the three cases against a vanilla dedicated server. Fixes #8779 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
757ce1bd14
commit
2f6d9c7dc4
3 files changed
+129
-2
No files matched your search
@@ -162,6 +162,11 @@ impl Game<'_> {
|
||||
if owner_breeze && m.species() == Species::Breeze {
|
||||
return;
|
||||
}
|
||||
// Refused before the hurt, so the kill is not credited
|
||||
// either; the burst of `wind_charge_hit` still goes off.
|
||||
if horizon_client::mobs::wither::armor_refuses(m, &p.kind) {
|
||||
return;
|
||||
}
|
||||
m.damage(1.0, source);
|
||||
}
|
||||
if p.owner == Some(Who::Player) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The wither (`WitherBoss`): eleven seconds of invulnerable charging that
|
||||
//! end in a blast, then flying after its target while three heads fire
|
||||
//! skulls, breaking blocks around it when hurt, immune to arrows below half
|
||||
//! health.
|
||||
//! skulls, breaking blocks around it when hurt, immune to arrows and a
|
||||
//! player's wind charges below half health.
|
||||
use super::{
|
||||
DamageKind, Mob, MobEvent, MobWorld, Species, Who,
|
||||
anim::{Poses, State},
|
||||
@@ -449,6 +449,14 @@ pub fn accepts(m: &mut Mob, kind: DamageKind, attacker: Option<Who>, others_unde
|
||||
m.extra.ints[5] += 3;
|
||||
true
|
||||
}
|
||||
/// `WitherBoss.hurtServer`: a powered wither's armor refuses a direct
|
||||
/// `WindCharge`. A breeze's `BreezeWindCharge` is not one (both only extend
|
||||
/// `AbstractWindCharge`), so it still hurts. Checked on the projectile, not on
|
||||
/// `DamageKind`: the `wind_charge` damage type alone has no direct charge
|
||||
/// (`/damage @s minecraft:wind_charge`) and goes through.
|
||||
pub fn armor_refuses(m: &Mob, direct: &ProjectileKind) -> bool {
|
||||
m.species() == Species::Wither && *direct == ProjectileKind::WindCharge && powered(m)
|
||||
}
|
||||
/// `WitherBossModel.setupAnim`.
|
||||
pub fn anim(m: &Mob, s: &State, p: &mut Poses) {
|
||||
let body =
|
||||
@@ -518,6 +526,40 @@ mod tests {
|
||||
assert!(!shielded_from(Species::Wither, Species::Creeper));
|
||||
assert!(!shielded_from(Species::Zombie, Species::Wither));
|
||||
}
|
||||
/// `WitherBoss.hurtServer` tests `instanceof WindCharge`: a powered wither
|
||||
/// took a point of damage from every charge (#8779), and a breeze's charge,
|
||||
/// a sibling class, has to keep hurting it.
|
||||
#[test]
|
||||
fn only_a_powered_withers_armor_refuses_a_wind_charge() {
|
||||
use crate::mobs::Damage;
|
||||
use crate::mobs::sensor_scan_tests::kind;
|
||||
use std::sync::Arc;
|
||||
let mob = |species, health: f32| {
|
||||
let mut kind = kind();
|
||||
Arc::make_mut(&mut kind).species = species;
|
||||
let mut m = Mob::new(1, kind, Vec3::ZERO, 0.0);
|
||||
m.attrs.max_health = 300.0;
|
||||
m.health = health;
|
||||
m
|
||||
};
|
||||
let (charge, breeze) = (ProjectileKind::WindCharge, ProjectileKind::BreezeWindCharge);
|
||||
// `isPowered` is `health <= max / 2`: the armor starts at exactly half.
|
||||
let half = mob(Species::Wither, 150.0);
|
||||
assert!(armor_refuses(&half, &charge));
|
||||
assert!(armor_refuses(&mob(Species::Wither, 1.0), &charge));
|
||||
assert!(!armor_refuses(&half, &breeze));
|
||||
assert!(!armor_refuses(&mob(Species::Wither, 150.5), &charge));
|
||||
assert!(!armor_refuses(&mob(Species::Zombie, 150.0), &charge));
|
||||
|
||||
// What `wind_charge_damage` lets through still lands on the armor...
|
||||
let mut m = mob(Species::Wither, 150.0);
|
||||
assert!(m.damage(1.0, Damage::of(DamageKind::Thrown)));
|
||||
assert!(m.health < 150.0);
|
||||
// ...and arrows stay refused by the damage filter.
|
||||
let mut m = mob(Species::Wither, 150.0);
|
||||
assert!(!m.damage(1.0, Damage::of(DamageKind::Arrow)));
|
||||
assert_eq!(m.health, 150.0);
|
||||
}
|
||||
/// A spawning wither rolled the three particles of `aiStep`'s
|
||||
/// `getInvulnerableTicks() > 0` block and dropped them on the floor: the
|
||||
/// potion-coloured (`0.7, 0.7, 0.9`) cloud around it never appeared (#7486).
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Wind charges meeting a wither, on a dedicated server, against vanilla's, seen by a vanilla client.
|
||||
|
||||
A vanilla dedicated server and Horizon's (`horizon-client --dedicated --nogui`) each serve a copy of one flat world
|
||||
vanilla generated first, with Alice an operator in survival. Three summoned withers (NoAI, so none heals, moves or
|
||||
shoots) each take one charge from above:
|
||||
|
||||
- at half health (`isPowered`), a `wind_charge`: `WitherBoss.hurtServer` refuses a direct `WindCharge`, and the charge
|
||||
is spent all the same;
|
||||
- at half health, a `breeze_wind_charge`: not a `WindCharge` (both only extend `AbstractWindCharge`), so it hurts;
|
||||
- above half health, a `wind_charge`: it hurts, which shows the charge does reach a wither.
|
||||
|
||||
Whether each wither kept its health is compared (vanilla's answer is the oracle). The health a hurt wither is left
|
||||
with is not asked: only that it changed.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
from . import test_locator_bar_parity as locator
|
||||
except ImportError:
|
||||
import test_locator_bar_parity as locator
|
||||
|
||||
NAME = 'wither-wind-charge-parity'
|
||||
# server_dir and generate_world write these server.properties.
|
||||
locator.PROPERTIES = dict(locator.PROPERTIES, gamemode='survival')
|
||||
|
||||
|
||||
def wither(tag, x, z, health):
|
||||
return f'cmd Alice summon minecraft:wither {x} -50 {z} {{NoAI:1b,Health:{health}f,Tags:["{tag}"]}}'
|
||||
|
||||
|
||||
def charge(kind, x, z):
|
||||
# Six blocks above the wither's feet, flying straight down: it hits within a few ticks.
|
||||
return f'cmd Alice summon minecraft:{kind} {x} -44 {z} {{Motion:[0.0,-1.0,0.0]}}'
|
||||
|
||||
|
||||
SCRIPT = [
|
||||
('setup', ['chat on', wither('armored', 12.5, 0.5, 100), wither('breeze', -11.5, 0.5, 100),
|
||||
wither('unarmored', 0.5, 12.5, 200)]),
|
||||
('hit', [charge('wind_charge', 12.5, 0.5), charge('breeze_wind_charge', -11.5, 0.5),
|
||||
charge('wind_charge', 0.5, 12.5)]),
|
||||
('asked', ['cmd Alice execute unless entity @e[type=minecraft:wind_charge]',
|
||||
'cmd Alice execute unless entity @e[type=minecraft:breeze_wind_charge]',
|
||||
'cmd Alice execute if entity @e[tag=armored,nbt={Health:100.0f}]',
|
||||
'cmd Alice execute if entity @e[tag=breeze]',
|
||||
'cmd Alice execute unless entity @e[tag=breeze,nbt={Health:100.0f}]',
|
||||
'cmd Alice execute if entity @e[tag=unarmored]',
|
||||
'cmd Alice execute unless entity @e[tag=unarmored,nbt={Health:200.0f}]']),
|
||||
]
|
||||
ANSWERS = 7
|
||||
|
||||
|
||||
def shown(lines):
|
||||
"""The answers about the charges and the withers."""
|
||||
out = []
|
||||
for line in lines:
|
||||
parts = line.split(' ', 3)
|
||||
if len(parts) >= 3 and parts[1] == 'step=asked' and parts[2] == 'chat' and 'conditional' in line:
|
||||
out.append(f'{parts[1]} {parts[3]}')
|
||||
return out
|
||||
|
||||
|
||||
def check(vanilla, vanilla_all):
|
||||
if sum('conditional.pass' in line for line in vanilla) != ANSWERS:
|
||||
return 'a charge did not hit its wither, or a wither did not take it as WitherBoss.hurtServer says'
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
lines = locator.one_player(NAME, SCRIPT, shown, check, 'Wind charges flying into summoned withers on a dedicated '
|
||||
'server: a powered wither refuses a wind_charge, and takes a breeze_wind_charge as '
|
||||
'one above half health takes a wind_charge.', step_seconds=1.0)
|
||||
print(f'wither wind charge: {len(lines)} lines identical')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except AssertionError as error:
|
||||
sys.exit(f'{NAME}: {error}')
|
||||
Reference in new issue
Block a user