mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-10 02:21:33 -10:00
Merge pull request #6053 from HrafnaValkyrja/fix/lightning-hits-all
🤖 Agent: Lightning tests every entity's box and destroys boats, carts and orbs as vanilla; main merged in (lan_e2e).
This commit is contained in:
2 files changed
+134
-12
No files matched your search
@@ -7,7 +7,11 @@
|
||||
use super::{Game, commands::Target};
|
||||
use horizon_client::advancements::EntityView;
|
||||
use horizon_client::mobs::{Damage, DamageKind, MobEvent, SpawnSetup, Species};
|
||||
use horizon_core::{position::BlockPos, random::RandomSource};
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Vec3},
|
||||
position::BlockPos,
|
||||
random::RandomSource,
|
||||
};
|
||||
|
||||
pub(super) struct Bolt {
|
||||
/// The entity id a server's players know it by (0 until it has one).
|
||||
@@ -452,10 +456,26 @@ impl Game<'_> {
|
||||
}
|
||||
});
|
||||
}
|
||||
/// `thunderHit` for entities within 3 blocks (up to 9 above). Returns the
|
||||
/// mobs in that box before the hit, as `CHANNELED_LIGHTNING` counts them.
|
||||
/// `thunderHit` for entities whose box meets the strike box, 3 blocks
|
||||
/// around and up to 9 above (`getEntities(this, AABB)` tests boxes, not
|
||||
/// positions). Returns the mobs in that box before the hit, as
|
||||
/// `CHANNELED_LIGHTNING` counts them. Boats, minecarts and experience
|
||||
/// orbs are hit too (`VehicleEntity.hurtServer` adds 5 x 10 > 40 and
|
||||
/// destroys them; an orb has 5 health).
|
||||
fn thunder_hit(&mut self, b: &mut Bolt) -> Vec<EntityView> {
|
||||
let at = b.at;
|
||||
let strike = Aabb::new(
|
||||
Vec3::new(at[0] - 3.0, at[1] - 3.0, at[2] - 3.0),
|
||||
Vec3::new(at[0] + 3.0, at[1] + 9.0, at[2] + 3.0),
|
||||
);
|
||||
// Whether the box of an entity standing at `p` (half width `w`,
|
||||
// height `h`) meets the strike box.
|
||||
let meets = move |p: [f64; 3], w: f64, h: f64| {
|
||||
strike.intersects(Aabb::new(
|
||||
Vec3::new(p[0] - w, p[1], p[2] - w),
|
||||
Vec3::new(p[0] + w, p[1] + h, p[2] + w),
|
||||
))
|
||||
};
|
||||
let inside = move |p: [f64; 3]| {
|
||||
(p[0] - at[0]).abs() <= 3.0
|
||||
&& (p[2] - at[2]).abs() <= 3.0
|
||||
@@ -465,7 +485,14 @@ impl Game<'_> {
|
||||
let struck: Vec<(u32, EntityView)> = self
|
||||
.mobs
|
||||
.iter()
|
||||
.filter(|m| m.alive() && inside([m.position.x, m.position.y, m.position.z]))
|
||||
.filter(|m| {
|
||||
m.alive()
|
||||
&& meets(
|
||||
[m.position.x, m.position.y, m.position.z],
|
||||
m.kind.width / 2.0,
|
||||
m.kind.height,
|
||||
)
|
||||
})
|
||||
.map(|m| (m.id, self.mob_entity(m)))
|
||||
.collect();
|
||||
b.hit.extend(struck.iter().map(|(id, _)| *id));
|
||||
@@ -477,7 +504,11 @@ impl Game<'_> {
|
||||
let mut events = Vec::new();
|
||||
let peaceful = self.ui.difficulty == 0;
|
||||
for m in self.mobs.iter_mut().filter(|m| m.alive()) {
|
||||
if !inside([m.position.x, m.position.y, m.position.z]) {
|
||||
if !meets(
|
||||
[m.position.x, m.position.y, m.position.z],
|
||||
m.kind.width / 2.0,
|
||||
m.kind.height,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let setup = |m: &horizon_client::mobs::Mob| SpawnSetup {
|
||||
@@ -554,10 +585,34 @@ impl Game<'_> {
|
||||
b.spared.extend(self.dropped[before..].iter().map(|e| e.id));
|
||||
}
|
||||
}
|
||||
// Every player in the box, the server's players too.
|
||||
let strike = move |g: &mut Self| {
|
||||
// Boats, then minecarts: `VehicleEntity.hurtServer` for 5 damage
|
||||
// (x 10 over the 40 a vehicle takes) destroys them. Lightning is no
|
||||
// fire source, so a TNT cart does not prime.
|
||||
let mut i = self.boats.len();
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
let bb = self.boats[i].bounds();
|
||||
if strike.intersects(bb) {
|
||||
self.hurt_boat(i, 5.0, false);
|
||||
}
|
||||
}
|
||||
let mut i = self.carts.len();
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
let bb = self.carts[i].bounds();
|
||||
if strike.intersects(bb) {
|
||||
self.hurt_cart(i, 5.0, false, false);
|
||||
}
|
||||
}
|
||||
// `ExperienceOrb.hurtServer`: 5 damage off 5 health.
|
||||
let orbs_before = self.orb_ids();
|
||||
self.orbs
|
||||
.retain_mut(|o| !(strike.intersects(o.bounds()) && o.hurt(5.0)));
|
||||
self.native_orbs_left_since(&orbs_before);
|
||||
// Every player in the box, the server's players too (0.6 x 1.8).
|
||||
let strike_players = move |g: &mut Self| {
|
||||
if let Some(p) = g.player.as_ref().map(|p| p.position)
|
||||
&& inside([p.x, p.y, p.z])
|
||||
&& meets([p.x, p.y, p.z], 0.3, 1.8)
|
||||
{
|
||||
if let Some(p) = g.player.as_mut() {
|
||||
p.fire_ticks += 1;
|
||||
@@ -570,18 +625,18 @@ impl Game<'_> {
|
||||
g.hurt_player_by("minecraft:lightning_bolt", 5.0);
|
||||
}
|
||||
};
|
||||
strike(self);
|
||||
self.each_guest_hit(strike);
|
||||
strike_players(self);
|
||||
self.each_guest_hit(strike_players);
|
||||
b.hit_items.extend(
|
||||
self.dropped
|
||||
.iter()
|
||||
.filter(|e| inside([e.position.x, e.position.y, e.position.z]))
|
||||
.filter(|e| meets([e.position.x, e.position.y, e.position.z], 0.125, 0.25))
|
||||
.map(|e| e.id),
|
||||
);
|
||||
// Dropped items burn up (ItemEntity health 5), save fire-resistant ones.
|
||||
let before = self.dropped_ids();
|
||||
self.dropped.retain(|e| {
|
||||
!inside([e.position.x, e.position.y, e.position.z])
|
||||
!meets([e.position.x, e.position.y, e.position.z], 0.125, 0.25)
|
||||
|| e.stack.item.contains("netherite")
|
||||
|| (e.age < 20 && b.spared.contains(&e.id))
|
||||
});
|
||||
|
||||
@@ -11156,6 +11156,73 @@ CHECKS += ['attack_over_slab']
|
||||
CHECK_FNS['attack_over_slab'] = check_attack_over_slab
|
||||
|
||||
|
||||
def check_lightning_box(host, guest, out):
|
||||
"""A lightning bolt tests the box of every entity against its strike box (3 around, 9 above),
|
||||
not their feet, and destroys boats, minecarts and experience orbs (LightningBolt.tick ->
|
||||
Entity.thunderHit -> VehicleEntity.hurtServer 5 x 10 > 40; ExperienceOrb health 5). Real
|
||||
vanilla 26.3 (same scene): boat, cart, orb and a boat whose feet stand 3.5 out are gone, a cow
|
||||
whose feet stand 3.2 out drops from 10 to 5, a boat 6 out is untouched."""
|
||||
x, y, z = (int(v // 1) for v in pos_of(host))
|
||||
ox, oy, oz = x + 12, y, z
|
||||
tag = 'dg_bolt'
|
||||
spots = {'boat': ('minecraft:oak_boat', 1.5, 0), 'cart': ('minecraft:minecart', -1.5, 0),
|
||||
'orb': ('minecraft:experience_orb', 0, -1.5), 'edgeboat': ('minecraft:oak_boat', 0, 3.5),
|
||||
'edgecow': ('minecraft:cow', 3.2, 0), 'farboat': ('minecraft:oak_boat', 6.0, 0)}
|
||||
markers = {name: (ox - 8 + i, oy, oz + 12) for i, name in enumerate(spots)}
|
||||
|
||||
def alive(name):
|
||||
if name == 'orb': # @e does not list experience orbs; the state counts them by kind
|
||||
return remote(host).get('entities', {}).get('minecraft:experience_orb', 0) > 0
|
||||
# Marker blocks: a command the game runs, a block the control reads back.
|
||||
mx, my, mz = markers[name]
|
||||
host('command', command=f'setblock {mx} {my} {mz} minecraft:air')
|
||||
host('command', command=f'execute if entity @e[tag={tag}_{name}] run setblock {mx} {my} {mz} minecraft:gold_block')
|
||||
host.wait(2)
|
||||
return host.raw('block', mx, my, mz).get('block') == 'minecraft:gold_block'
|
||||
|
||||
seen = {}
|
||||
try:
|
||||
host('command', command='gamemode creative')
|
||||
host('command', command='difficulty normal')
|
||||
host('command', command=f'fill {ox - 9} {oy - 1} {oz - 9} {ox + 9} {oy - 1} {oz + 14} minecraft:stone')
|
||||
host('command', command=f'fill {ox - 9} {oy} {oz - 9} {ox + 9} {oy + 6} {oz + 14} minecraft:air')
|
||||
host('command', command=f'kill @e[tag={tag}]')
|
||||
for name, (kind, dx, dz) in spots.items():
|
||||
extra = ',NoAI:1b,PersistenceRequired:1b' if kind == 'minecraft:cow' else ''
|
||||
host('command', command=f'summon {kind} {ox + 0.5 + dx} {oy} {oz + 0.5 + dz} '
|
||||
f'{{Tags:["{tag}","{tag}_{name}"]{extra}}}')
|
||||
host.wait(10)
|
||||
seen['before'] = {name: alive(name) for name in spots}
|
||||
assert all(seen['before'].values()), f'the command cannot see every entity before the bolt: {seen}'
|
||||
def cow():
|
||||
near = [m for m in host.raw('entities', 20, 'cow')['entities']
|
||||
if math.dist(m['pos'], (ox + 3.7, oy, oz + 0.5)) < 1.0]
|
||||
return near[0]['health'] if near else 0.0
|
||||
seen['cow_before'] = cow()
|
||||
host('command', command=f'summon minecraft:lightning_bolt {ox + 0.5} {oy} {oz + 0.5}')
|
||||
seen['cow_series'] = []
|
||||
for _ in range(8):
|
||||
host.wait(1)
|
||||
seen['cow_series'].append(cow())
|
||||
host.wait(2)
|
||||
seen['after'] = {name: alive(name) for name in spots}
|
||||
seen['cow_after'] = min(seen['cow_series'])
|
||||
host.shot(out, 'lightning-box')
|
||||
finally:
|
||||
host('command', command=f'kill @e[tag={tag}]')
|
||||
# Vanilla: everything but the far boat is gone; the cow is hurt.
|
||||
# (The cow burns on after the strike, so only its health at the strike is compared.)
|
||||
gone = {k: v for k, v in seen['after'].items() if k != 'edgecow'}
|
||||
assert gone == {'boat': False, 'cart': False, 'orb': False, 'edgeboat': False, 'farboat': True}, \
|
||||
f'who survived the bolt: {seen}'
|
||||
assert seen['cow_series'][0] == 5.0, f'the cow with its feet 3.2 out was not struck for 5: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['lightning-box']
|
||||
CHECK_FNS['lightning-box'] = check_lightning_box
|
||||
|
||||
|
||||
def check_boat_submerged(host, guest, out):
|
||||
"""AbstractBoat.tick ejects players after 60 consecutive underwater ticks on the server."""
|
||||
hx, _, hz = pos_of(host)
|
||||
|
||||
Reference in new issue
Block a user