mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11133 from Ragbie/fix/enderman-hurt-teleport-now
An enderman hurt by the game layer teleports at the hurt, as vanilla's does
This commit is contained in:
8 files changed
+257
No files matched your search
@@ -125,6 +125,8 @@ mod enchant_effects;
|
||||
mod end;
|
||||
mod enderman;
|
||||
#[cfg(test)]
|
||||
mod enderman_hurt_teleport_tests;
|
||||
#[cfg(test)]
|
||||
mod enderman_placement_tests;
|
||||
mod entity;
|
||||
mod entity_culling;
|
||||
@@ -8433,6 +8435,8 @@ impl Game<'_> {
|
||||
horizon_client::spans::time("tick_block_entity_anims", || {
|
||||
self.tick_block_entity_anims()
|
||||
});
|
||||
// A conduit's hurt teleports an enderman now (`Enderman.hurtServer`).
|
||||
self.settle_hurt_teleports()?;
|
||||
horizon_client::spans::time("tick_particles", || self.tick_particles());
|
||||
self.tick_client_ambient();
|
||||
}
|
||||
|
||||
@@ -502,6 +502,10 @@ impl Game<'_> {
|
||||
for (mob, target) in reinforcements {
|
||||
self.call_reinforcements(mob, target);
|
||||
}
|
||||
// Enderman.hurtServer's teleport, refused hit or not, before the command returns.
|
||||
if let Err(e) = self.settle_hurt_teleports() {
|
||||
log::warn!("Horizon: /damage: enderman teleport: {e}");
|
||||
}
|
||||
hurt
|
||||
}
|
||||
|
||||
|
||||
@@ -1014,6 +1014,9 @@ impl Game<'_> {
|
||||
}
|
||||
/// Advance every mob one tick, then apply their events.
|
||||
pub(super) fn tick_creatures(&mut self) -> Result<()> {
|
||||
// Hurts dealt since the last creature tick (explosions, commands, the
|
||||
// systems after it) teleport an enderman before it ticks, not in its AI step.
|
||||
self.settle_hurt_teleports()?;
|
||||
self.attack_ticks = self.attack_ticks.saturating_add(1);
|
||||
self.refresh_gear_knockback_resistance();
|
||||
self.tick_leads();
|
||||
@@ -1513,6 +1516,8 @@ impl Game<'_> {
|
||||
queued.append(&mut m.outbox);
|
||||
}
|
||||
self.handle_mob_events(queued)?;
|
||||
// The enderman teleports those hurts asked for (`Enderman.hurtServer`).
|
||||
self.settle_hurt_teleports()?;
|
||||
laps.lap("creature_outbox");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,50 @@ use horizon_world::{
|
||||
};
|
||||
|
||||
impl Game<'_> {
|
||||
/// `Enderman.hurtServer` teleports inside the hurt (`teleport()`,
|
||||
/// `repeatedlyTryToTeleport()`). `Mob::damage` has no world, so it leaves
|
||||
/// the tries in `counter2`; a hurt the game layer deals runs them here,
|
||||
/// right after it, not on the enderman's next AI step.
|
||||
pub(super) fn settle_hurt_teleports(&mut self) -> Result<()> {
|
||||
let pending: Vec<u32> = self
|
||||
.mobs
|
||||
.iter()
|
||||
.filter(|m| m.species() == Species::Enderman && m.extra.counter2 > 0)
|
||||
.map(|m| m.id)
|
||||
.collect();
|
||||
for id in pending {
|
||||
// What `LivingEntity.hurtServer` raised comes first, where the
|
||||
// enderman was hurt and from its random's next draws: the hurt
|
||||
// sound, `InfestedMobEffect.onMobHurt`'s silverfish.
|
||||
let Some(i) = self.mobs.iter().position(|m| m.id == id) else {
|
||||
continue;
|
||||
};
|
||||
let hurt = std::mem::take(&mut self.mobs[i].outbox);
|
||||
if !hurt.is_empty() {
|
||||
self.handle_mob_events(hurt)?;
|
||||
}
|
||||
// The world is read while the enderman moves: take it out, then
|
||||
// put it back where it was (the mob order stays).
|
||||
let Some(i) = self.mobs.iter().position(|m| m.id == id) else {
|
||||
continue;
|
||||
};
|
||||
let mut mob = self.mobs.swap_remove(i);
|
||||
let mut events = Vec::new();
|
||||
{
|
||||
let world = self.mob_world_base(&[]);
|
||||
horizon_client::mobs::enderman::run_pending_teleports(
|
||||
&mut mob,
|
||||
&world,
|
||||
&mut events,
|
||||
);
|
||||
}
|
||||
self.mobs.push(mob);
|
||||
let last = self.mobs.len() - 1;
|
||||
self.mobs.swap(i, last);
|
||||
self.handle_mob_events(events)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// `EndermanLeaveBlockGoal.tick`: normalize, check, place, then clear.
|
||||
pub(super) fn enderman_place_block(
|
||||
&mut self,
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
//! `Enderman.hurtServer` teleports inside the hurt the game layer deals.
|
||||
use super::{
|
||||
Audio, Game, Options, Progress, Settings, explosion::BlockInteraction, load_resources,
|
||||
};
|
||||
use horizon_client::{
|
||||
chunk::Chunk,
|
||||
mobs::{Mob, MobEvent},
|
||||
};
|
||||
use horizon_core::{
|
||||
geometry::Vec3,
|
||||
position::ChunkPos,
|
||||
random::{LegacyRandom, RandomSource},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A game over 5x5 empty chunks around (0, 0) with a stone floor at y = 3:
|
||||
/// every enderman teleport from (8.5, 4, 8.5) lands on it or fails.
|
||||
fn floored_game(root: &std::path::Path) -> Game<'static> {
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
root.to_path_buf(),
|
||||
None,
|
||||
root,
|
||||
&settings,
|
||||
std::time::Instant::now(),
|
||||
false,
|
||||
true,
|
||||
&Progress::new(),
|
||||
&mut |_, _| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
|
||||
options.resources = root.to_path_buf();
|
||||
options.execution_role = super::execution_role::ExecutionRole::IntegratedServer;
|
||||
let resources = Box::leak(Box::new(loaded.resources));
|
||||
let mut game = Game::new(
|
||||
resources,
|
||||
loaded.pack,
|
||||
loaded.gui.unwrap(),
|
||||
options,
|
||||
settings,
|
||||
Audio::silent(root),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
for cx in -2..=2 {
|
||||
for cz in -2..=2 {
|
||||
game.world
|
||||
.chunks
|
||||
.insert((cx, cz), Arc::new(Chunk::empty(cx, cz)));
|
||||
game.block_ticks
|
||||
.add_empty_chunk(ChunkPos::new(cx, cz))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
let stone = game
|
||||
.pack
|
||||
.assets
|
||||
.registry
|
||||
.default_state("minecraft:stone")
|
||||
.unwrap();
|
||||
for x in -32..48 {
|
||||
for z in -32..48 {
|
||||
game.world.set(x, 3, z, stone).unwrap();
|
||||
}
|
||||
}
|
||||
game
|
||||
}
|
||||
|
||||
fn enderman(game: &Game, id: u32, at: Vec3, seed: i64) -> Mob {
|
||||
let kind = game
|
||||
.res
|
||||
.mob_types()
|
||||
.get("minecraft:enderman")
|
||||
.unwrap()
|
||||
.clone();
|
||||
let mut mob = Mob::new(id, kind, at, 0.0);
|
||||
mob.random = LegacyRandom::new(seed);
|
||||
mob.persistent = true;
|
||||
mob
|
||||
}
|
||||
|
||||
/// `ServerExplosion.hurtEntities` -> `Enderman.hurtServer`: an enderman the
|
||||
/// blast hurts has teleported (or drawn not to) when the explosion returns.
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn an_explosion_teleports_endermen_before_it_returns() {
|
||||
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let mut game = floored_game(&root);
|
||||
let center = Vec3::new(8.5, 4.0, 8.5);
|
||||
for k in 0..12 {
|
||||
let angle = f64::from(k) * std::f64::consts::TAU / 12.0;
|
||||
let at = Vec3::new(
|
||||
center.x + 2.5 * angle.cos(),
|
||||
center.y,
|
||||
center.z + 2.5 * angle.sin(),
|
||||
);
|
||||
let mob = enderman(&game, 500 + k as u32, at, 900 + i64::from(k));
|
||||
game.mobs.push(mob);
|
||||
}
|
||||
let before: Vec<Vec3> = game.mobs.iter().map(|m| m.position).collect();
|
||||
game.server_explode(
|
||||
[center.x, center.y, center.z],
|
||||
4.0,
|
||||
BlockInteraction::Keep,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let pending: Vec<u32> = game
|
||||
.mobs
|
||||
.iter()
|
||||
.filter(|m| m.extra.counter2 > 0)
|
||||
.map(|m| m.id)
|
||||
.collect();
|
||||
assert!(
|
||||
pending.is_empty(),
|
||||
"teleports left for the AI step: {pending:?}"
|
||||
);
|
||||
let moved = game
|
||||
.mobs
|
||||
.iter()
|
||||
.zip(&before)
|
||||
.filter(|(m, b)| m.position.distance(**b) > 3.0)
|
||||
.count();
|
||||
assert!(moved > 0, "no enderman teleported");
|
||||
}
|
||||
|
||||
/// `LivingEntity.hurtServer` runs `InfestedMobEffect.onMobHurt` before
|
||||
/// `Enderman.hurtServer` teleports: the silverfish come out where the enderman
|
||||
/// was hurt, from its random's next draws.
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn an_infested_endermans_silverfish_come_out_where_it_was_hurt() {
|
||||
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let mut game = floored_game(&root);
|
||||
let at = Vec3::new(8.5, 4.0, 8.5);
|
||||
// A seed whose first draw passes the effect's 1-in-10 chance (`nextFloat() <= 0.1`).
|
||||
let seed = (0..)
|
||||
.find(|&s| LegacyRandom::new(s).next_float() <= 0.1)
|
||||
.unwrap();
|
||||
let mut mob = enderman(&game, 600, at, seed);
|
||||
mob.add_effect("minecraft:infested", 200, 0);
|
||||
// What `Mob::damage` leaves for a projectile's hit: the effect's spawn and
|
||||
// `repeatedlyTryToTeleport`.
|
||||
mob.outbox.push(MobEvent::Infested { mob: 600 });
|
||||
mob.extra.counter2 = 64;
|
||||
game.mobs.push(mob);
|
||||
game.settle_hurt_teleports().unwrap();
|
||||
let ender = game.mobs.iter().find(|m| m.id == 600).unwrap();
|
||||
assert!(
|
||||
ender.position.distance(at) > 3.0,
|
||||
"the enderman did not teleport"
|
||||
);
|
||||
let silverfish: Vec<Vec3> = game
|
||||
.mobs
|
||||
.iter()
|
||||
.filter(|m| m.kind.id == "minecraft:silverfish")
|
||||
.map(|m| m.position)
|
||||
.collect();
|
||||
assert!(!silverfish.is_empty(), "no silverfish");
|
||||
for p in silverfish {
|
||||
assert!(p.distance(at) < 3.0, "silverfish at {p:?}, hurt at {at:?}");
|
||||
}
|
||||
}
|
||||
@@ -672,6 +672,11 @@ impl Game<'_> {
|
||||
// its base is 0 for every mob type, so iron golems and
|
||||
// ravagers are pushed.
|
||||
hurt_mob(m, hurtable, cause, (amount, source));
|
||||
// Enderman.hurtServer teleports inside the hurt, before
|
||||
// the push (the mob order stays as it was).
|
||||
if let Err(e) = self.settle_hurt_teleports() {
|
||||
log::warn!("Horizon: explosion: enderman teleport: {e}");
|
||||
}
|
||||
}
|
||||
// `ItemEntity.hurtServer`: the damage comes off `Health`
|
||||
// (5 to start) in f32 and the item is destroyed at zero, so
|
||||
|
||||
@@ -27,6 +27,10 @@ impl Game<'_> {
|
||||
impact.amount,
|
||||
mob_kind,
|
||||
);
|
||||
// Enderman.hurtServer: a falling block is no living attacker.
|
||||
if let Err(e) = self.settle_hurt_teleports() {
|
||||
log::warn!("Horizon: falling block: enderman teleport: {e}");
|
||||
}
|
||||
let kind = if stalactite {
|
||||
"minecraft:falling_stalactite"
|
||||
} else if anvil {
|
||||
|
||||
@@ -932,6 +932,32 @@ def item_command_damage():
|
||||
return ops + setup('/kill @e[tag=item_hurt_actor]', '/scoreboard objectives remove item_hurt', '/tick unfreeze')
|
||||
|
||||
|
||||
def enderman_command_damage():
|
||||
"""DamageCommand -> Enderman.hurtServer: with no living entity behind the damage,
|
||||
random.nextInt(10) != 0 teleports the enderman then and there (teleport()), also when
|
||||
the hurt cooldown refuses the hit. The world is frozen, so no AI step does it later. The
|
||||
enderman floats (NoGravity) 32 blocks over a stone floor, so its teleport lands on it."""
|
||||
ops = setup('/forceload add 128 128 223 223', '/fill 144 199 144 208 199 208 minecraft:stone')
|
||||
# Every chunk a target can land in holds its entities (else @e no longer sees it there).
|
||||
ops += [{'op': 'settle', 'required_chunks': [[x, z] for x in range(9, 14) for z in range(9, 14)]}]
|
||||
ops += setup('/tick freeze')
|
||||
ender = '@e[tag=hurt_tp,limit=1]'
|
||||
ops += setup('/summon enderman 176.5 232 176.5 {Tags:["hurt_tp"],NoGravity:1b,PersistenceRequired:1b}')
|
||||
ops += [cmd(f'/execute if entity {ender}', expect_result=1)]
|
||||
# Nine draws in ten teleport. Hit again only while it is still where it was (a second
|
||||
# teleport from the floor could leave it): four leave it in place one time in 10^4.
|
||||
origin = '/execute positioned 176.5 232 176.5 as @e[tag=hurt_tp,distance=..0.5] run '
|
||||
ops += [cmd(f'/damage {ender} 1')]
|
||||
ops += setup(*[origin + 'damage @s 1'] * 3)
|
||||
ops += [cmd(f'/execute if entity {ender}', expect_result=1),
|
||||
cmd('/execute if entity @e[tag=hurt_tp,x=144,y=200,z=144,dx=64,dy=0,dz=64]', expect_result=1),
|
||||
cmd('/execute positioned 176.5 232 176.5 unless entity @e[tag=hurt_tp,distance=..0.5]',
|
||||
expect_result=1)]
|
||||
ops += setup('/kill @e[tag=hurt_tp]', '/fill 144 199 144 208 199 208 minecraft:air',
|
||||
'/forceload remove 128 128 223 223', '/tick unfreeze')
|
||||
return ops
|
||||
|
||||
|
||||
def snbt_trailing_commas():
|
||||
"""SnbtGrammar: a compound, a list and a typed array may end with a comma; an empty one
|
||||
may not hold only a comma, and two commas in a row are an error."""
|
||||
@@ -5990,6 +6016,7 @@ def groups():
|
||||
('mannequin pose anchors', mannequin_pose_anchors()),
|
||||
('item command damage', item_command_damage()),
|
||||
('dragon command damage', dragon_command_damage()),
|
||||
('enderman command damage', enderman_command_damage()),
|
||||
('SNBT trailing commas', snbt_trailing_commas()),
|
||||
('execute', execute()),
|
||||
('projectile origin', projectile_origin()),
|
||||
|
||||
Reference in new issue
Block a user