mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Wither: stay still while charging, and spare itself from its spawn blast
While a wither charges (Invul > 0), WitherBoss.customServerAiStep returns before setAlternativeTarget(0, ...), so aiStep has no target to fly to. Horizon stored the target's position first, so the charging wither flew toward whatever it targeted. The position is now taken after the charge. The blast that ends the charge is level.explode(this, ...): the wither is the source and ServerExplosion's getEntities(source, box) leaves it out. The wither now names itself as the explosion source (as a creeper does), and the blast's mob loop skips a source named that way, so it is neither hurt nor pushed. A source only guessed from the mobs near the centre (a sulfur cube, already off the list) may be a bystander and is still hurt. The command_exec corpus gets a "wither spawn" group: a wither summoned with Invul:20 next to a pig, its position mid-charge and its health after the blast. Fixes #3322 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8f0c178957
commit
ab9ceb6a6d
5 files changed
+51
-5
No files matched your search
@@ -57,6 +57,10 @@ pub(super) struct CombatTracker {
|
||||
/// The mob exploding now (`ServerExplosion.source`): its team's friendly
|
||||
/// fire decides whom the blast may hurt.
|
||||
pub(super) explosion_source: Option<u32>,
|
||||
/// The exploding mob named itself (`MobEvent::ExplosionSource`), so the
|
||||
/// blast leaves it out (`getEntities(source, box)`). A source guessed
|
||||
/// from the mobs near the centre is still hurt: it may be a bystander.
|
||||
pub(super) explosion_source_named: bool,
|
||||
/// The blast now going off comes from a primed TNT, creeper, wither
|
||||
/// skull, wither or TNT minecart, the sources that empty a hive they
|
||||
/// destroy (`BeehiveBlock.getDrops`).
|
||||
|
||||
@@ -1301,6 +1301,7 @@ impl Game<'_> {
|
||||
self.note_attacker("minecraft:explosion", id);
|
||||
}
|
||||
self.combat.explosion_source = source_mob;
|
||||
self.combat.explosion_source_named = noted;
|
||||
// Only a creeper or a wither empties the hives it blows up.
|
||||
// The creeper is already gone from the list, so the event
|
||||
// carries its kind.
|
||||
@@ -1310,6 +1311,7 @@ impl Game<'_> {
|
||||
self.explode(at, radius)
|
||||
};
|
||||
self.combat.explosion_source = None;
|
||||
self.combat.explosion_source_named = false;
|
||||
result?
|
||||
}
|
||||
MobEvent::AttackMob {
|
||||
|
||||
@@ -382,9 +382,14 @@ impl Game<'_> {
|
||||
}
|
||||
self.lan = Some(host);
|
||||
}
|
||||
// Mobs.
|
||||
// Mobs, but not the one exploding (`getEntities(source, box)`): a
|
||||
// wither's spawn blast neither hurts nor pushes it.
|
||||
let spared = exploding.filter(|_| self.combat.explosion_source_named);
|
||||
let mut mobs = std::mem::take(&mut self.mobs);
|
||||
for m in mobs.iter_mut().filter(|m| m.alive()) {
|
||||
for m in mobs
|
||||
.iter_mut()
|
||||
.filter(|m| m.alive() && spared != Some(m.id))
|
||||
{
|
||||
let hurtable = teams_allow(self, self.mob_team(m.id));
|
||||
let feet = [m.position.x, m.position.y, m.position.z];
|
||||
let dist = distance(feet) / diameter;
|
||||
|
||||
@@ -241,12 +241,20 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
/// `WitherBoss.customServerAiStep`: charging, then the side heads' targets
|
||||
/// and volleys, block breaking and healing.
|
||||
pub fn custom_server_ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.extra.point = m
|
||||
.target_info(w)
|
||||
.map(|t| [t.position.x, t.position.y, t.position.z]);
|
||||
if m.extra.counter > 0 {
|
||||
let left = m.extra.counter - 1;
|
||||
if left <= 0 {
|
||||
// `level.explode(this, ...)`: the wither is the blast's source, so
|
||||
// its entity query (`getEntities(source, box)`) leaves it out.
|
||||
let name = match &m.custom_name {
|
||||
Some(name) => crate::text::Text::literal(name.clone()),
|
||||
None => crate::text::Text::translate("entity.minecraft.wither", &[]),
|
||||
};
|
||||
events.push(MobEvent::ExplosionSource {
|
||||
mob: m.id,
|
||||
charged: false,
|
||||
name,
|
||||
});
|
||||
events.push(MobEvent::Explode(
|
||||
[m.position.x, m.eye_y(), m.position.z],
|
||||
7.0,
|
||||
@@ -268,6 +276,11 @@ pub fn custom_server_ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEven
|
||||
}
|
||||
return;
|
||||
}
|
||||
// `setAlternativeTarget(0, ...)` once charged: `aiStep` flies toward this
|
||||
// target only after the charge, never during it.
|
||||
m.extra.point = m
|
||||
.target_info(w)
|
||||
.map(|t| [t.position.x, t.position.y, t.position.z]);
|
||||
// The side heads pick targets and fire on their own.
|
||||
let age = m.age as i32;
|
||||
for i in 1..3 {
|
||||
|
||||
@@ -349,6 +349,27 @@ def entities():
|
||||
]
|
||||
|
||||
|
||||
def wither_spawn():
|
||||
"""A wither charging up (`Invul`) neither moves toward its target nor is hurt by
|
||||
the blast that ends the charge (#3322): `WitherBoss.customServerAiStep` sets no
|
||||
alternative target while invulnerable, and `ServerLevel.explode`'s entity query
|
||||
skips the source."""
|
||||
wither = '@e[type=minecraft:wither,limit=1]'
|
||||
return [
|
||||
*setup('/fill 0 209 4 9 209 20 minecraft:stone',
|
||||
'/summon minecraft:pig 4.5 210 18.5 {NoAI:1b,Tags:["wither_prey"]}',
|
||||
'/summon minecraft:wither 4.5 210 8.5 {Invul:20}'),
|
||||
{'op': 'tick', 'n': 10},
|
||||
cmd(f'/data get entity {wither} Pos'),
|
||||
cmd(f'/data get entity {wither} Invul'),
|
||||
{'op': 'tick', 'n': 12},
|
||||
cmd(f'/data get entity {wither} Invul'),
|
||||
cmd(f'/data get entity {wither} Health'),
|
||||
*setup('/kill @e[type=minecraft:wither]', '/kill @e[tag=wither_prey]', '/fill 0 200 0 15 228 20 air'),
|
||||
{'op': 'tick', 'n': 5},
|
||||
]
|
||||
|
||||
|
||||
def riding_teleport():
|
||||
"""Player teleports remove the vehicle, leave it behind and keep the destination (#1473)."""
|
||||
horse = '@e[tag=tp_horse,limit=1]'
|
||||
@@ -1122,6 +1143,7 @@ def groups():
|
||||
('player state', player_state()),
|
||||
('blocks and border', world_blocks()),
|
||||
('entities', entities()),
|
||||
('wither spawn', wither_spawn()),
|
||||
('riding teleport', riding_teleport()),
|
||||
('mob riding', mob_riding()),
|
||||
('scores, teams and bossbars', scores()),
|
||||
|
||||
Reference in new issue
Block a user