# Conflicts:
#	crates/horizon-client/src/game/potions.rs
This commit is contained in:
Ludovic Marie committed 2026-10-27 07:15:26 +01:00
commit 65b0cb2521
5 files changed
+246 -20

No files matched your search

+15 -6
View File
@@ -8301,15 +8301,24 @@ impl Game<'_> {
// The hurt or death sound the mob raised (getHurtSound / getDeathSound,
// its voice pitch from its own random).
let queued = std::mem::take(&mut self.mobs[index].outbox);
let mut infested = false;
for event in queued {
if let horizon_client::mobs::MobEvent::Sound(s, at, volume, pitch) = event {
self.audio
.play(&format!("minecraft:{s}"), Some(at), volume, pitch);
} else {
// Left for the end of the tick.
self.mobs[index].outbox.push(event);
match event {
horizon_client::mobs::MobEvent::Sound(s, at, volume, pitch) => {
self.audio
.play(&format!("minecraft:{s}"), Some(at), volume, pitch);
}
horizon_client::mobs::MobEvent::Infested { .. } => infested = true,
other => {
// Left for the end of the tick.
self.mobs[index].outbox.push(other);
}
}
}
if infested {
let id = self.mobs[index].id;
self.mob_infested(id);
}
self.weapon_hit(index);
// Player.attack: causeFoodExhaustion(0.1F) only when the hit hurt
// (inside `if (wasHurt)`), after damageStatsAndHearts.
+4
View File
@@ -156,6 +156,10 @@ impl Game<'_> {
direct,
});
self.combat.last_damage = now;
// InfestedMobEffect.onMobHurt.
if self.potions.effects.get("infested").is_some() {
self.player_infested();
}
}
self.combat.pending.clear();
self.combat.pending_direct.clear();
+47 -10
View File
@@ -527,6 +527,7 @@ impl Game<'_> {
let mut events = Vec::new();
let mut mobs = std::mem::take(&mut self.mobs);
let mut dead = Vec::new();
let mut corpses = Vec::new();
let mut died = std::mem::take(&mut self.died);
let ticking = self.entity_ticking_area();
// With guests on, each mob sees its nearest player (game/lan_mobs.rs).
@@ -621,6 +622,11 @@ impl Game<'_> {
horizon_client::mobs::apply_mob_pushes(m, &mut pushes);
let from = events.len();
let kept = m.tick(&w, &mut scratch, &mut events);
// A killed mob's body is removed: its effects that act on
// death run (`LivingEntity.remove` with KILLED).
if !kept && !m.alive() && !m.removed && !m.extra.effects.is_empty() {
corpses.push(m.clone());
}
horizon_client::mobs::take_mob_pushes(&mut events, from, &mut pushes);
// LivingEntity.tickDeath: after twenty ticks the body is removed.
if !kept && !m.removed && !m.alive() && m.death_time >= 20 {
@@ -700,6 +706,16 @@ impl Game<'_> {
for m in dead {
self.mob_died(&m);
}
for mut m in corpses {
let names: Vec<&'static str> = m
.extra
.effects
.iter()
.map(|e| e.0.trim_start_matches("minecraft:"))
.collect();
let bounds = m.bounds();
self.effects_on_death(&names, bounds, false, Some(m.id), Some(&mut m.random));
}
self.handle_mob_events(events)?;
self.lan_guest_mob_events(guest_events);
self.tick_projectiles()?;
@@ -903,6 +919,18 @@ impl Game<'_> {
}
}
}
/// `InfestedMobEffect.onMobHurt` for a mob: silverfish from half its height up.
pub(super) fn mob_infested(&mut self, mob: u32) {
if let Some(m) = self.mobs.iter().find(|m| m.id == mob) {
let at = [
m.position.x,
m.position.y + f64::from(m.height()) / 2.0,
m.position.z,
];
let look = m.view_vector();
self.infested_hurt(at, look, Some(mob));
}
}
/// `LivingEntity.dropFromGiftLootTable`: the gift table from its random
/// sequence with the mob as `this` (a chicken's egg depends on its
/// variant); the loot data of the game's resources decides, and without it
@@ -1049,6 +1077,7 @@ impl Game<'_> {
} => {
let mut killed = false;
let mut hurt = false;
let mut infested = false;
if let Some(t) = self.mobs.iter_mut().find(|m| m.id == target) {
let hit = t.damage(amount, source);
hurt = hit;
@@ -1057,20 +1086,27 @@ impl Game<'_> {
// The hurt or death sound the mob raised.
let center = t.center();
for event in std::mem::take(&mut t.outbox) {
if let MobEvent::Sound(s, _, volume, pitch) = event {
self.audio.play(
&format!("minecraft:{s}"),
Some(center),
volume,
pitch,
);
} else {
// Left for the end of the tick.
t.outbox.push(event);
match event {
MobEvent::Sound(s, _, volume, pitch) => {
self.audio.play(
&format!("minecraft:{s}"),
Some(center),
volume,
pitch,
);
}
MobEvent::Infested { .. } => infested = true,
other => {
// Left for the end of the tick.
t.outbox.push(other);
}
}
}
}
}
if infested {
self.mob_infested(target);
}
if hurt
&& let Some(Who::Mob(attacker)) = source.attacker
&& let Some(m) = self
@@ -1116,6 +1152,7 @@ impl Game<'_> {
}
MobEvent::BecomeStatue { mob } => self.golem_become_statue(mob),
MobEvent::EnterHive { mob, hive } => self.bee_enter_hive(mob, hive),
MobEvent::Infested { mob } => self.mob_infested(mob),
MobEvent::Gift { target, item } => {
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == target)
&& m.saddle_item().is_none()
+173 -4
View File
@@ -21,8 +21,8 @@ use horizon_client::{
text::Text,
};
use horizon_core::{
geometry::{Aabb, Vec3},
random::RandomSource,
geometry::{Aabb, Direction, Vec3},
random::{LegacyRandom, RandomSource},
};
/// A thrown splash or lingering potion in flight.
@@ -136,6 +136,153 @@ fn splash_distance_sqr(at: Vec3, age: u32, bounds: Aabb) -> Option<f64> {
(d2 < 16.0).then_some(d2)
}
impl Game<'_> {
/// `LivingEntity.triggerOnDeathMobEffects` (`onMobRemoved` with the reason
/// KILLED) for what Oozing, Weaving and Wind Charged do when their bearer
/// dies. `bearer` is the dying mob's id, `player` whether it is a player,
/// `random` the dying mob's own (`mob.getRandom()`; the world's for the
/// player).
pub(super) fn effects_on_death(
&mut self,
effects: &[&str],
bounds: Aabb,
player: bool,
bearer: Option<u32>,
mut random: Option<&mut LegacyRandom>,
) {
let feet = bounds.bottom_center();
if effects.contains(&"oozing") {
// OozingMobEffect.numberOfSlimesToSpawn: two, fewer when the
// slimes close by already fill the cramming limit.
let limit = self.ui.rules.int("max_entity_cramming");
let mut count = 2;
if limit >= 1 {
let near = bounds.inflate(Vec3::new(2.0, 2.0, 2.0));
let nearby = self
.mobs
.iter()
.filter(|m| {
m.species() == horizon_client::mobs::Species::Slime
&& Some(m.id) != bearer
&& m.bounds().intersects(near)
})
.count()
.min(limit as usize) as i32;
count = (limit - nearby).clamp(0, count);
}
for _ in 0..count {
// spawnSlimeOffspring: a size 2 slime half a block up.
if let Some(id) = self.spawn_mob_for(
"minecraft:slime",
[feet.x, feet.y + 0.5, feet.z],
false,
true,
) && let Some(m) = self.mobs.iter_mut().find(|m| m.id == id)
{
horizon_client::mobs::cube::set_size(m, 2);
}
}
}
if effects.contains(&"weaving") && (player || self.ui.rules.bool("mob_griefing")) {
self.spawn_cobwebs_around(
[
feet.x.floor() as i32,
feet.y.floor() as i32,
feet.z.floor() as i32,
],
random.as_deref_mut(),
);
}
if effects.contains(&"wind_charged") {
// A burst centred at half the height, radius 3 to 5.
let center = [feet.x, feet.y + bounds.size().y / 2.0, feet.z];
let r = random.unwrap_or(&mut self.world.random);
let radius = 3.0 + r.next_float() * 2.0;
let _ = self.wind_burst(center, radius, 1.0, false, false);
}
}
/// `WeavingMobEffect.spawnCobwebsRandomlyAround`: two or three cobwebs in
/// free spaces with a solid floor, picked among 15 random blocks within one
/// block of `pos`.
fn spawn_cobwebs_around(&mut self, pos: [i32; 3], mut random: Option<&mut LegacyRandom>) {
let r = random.as_deref_mut().unwrap_or(&mut self.world.random);
let wanted = 2 + r.next_int_bound(2).unwrap_or(0) as usize;
let mut chosen: Vec<[i32; 3]> = Vec::new();
for _ in 0..15 {
let r = random.as_deref_mut().unwrap_or(&mut self.world.random);
let at = [
pos[0] - 1 + r.next_int_bound(3).unwrap_or(0),
pos[1] - 1 + r.next_int_bound(3).unwrap_or(0),
pos[2] - 1 + r.next_int_bound(3).unwrap_or(0),
];
let facts = self.res.facts();
let here = facts.get(self.world.block(at[0], at[1], at[2]));
let below = facts.get(self.world.block(at[0], at[1] - 1, at[2]));
if !chosen.contains(&at)
&& here.replaceable
&& below.sturdy_full & (1 << Direction::Up as u8) != 0
{
chosen.push(at);
if chosen.len() >= wanted {
break;
}
}
}
for at in chosen {
if self.set_state_named(at, "minecraft:cobweb", &[]).is_ok() {
self.level_event_at(3018, at, 0);
}
}
}
/// `InfestedMobEffect.onMobHurt` for the player: from half its height up,
/// along where it looks.
pub(super) fn player_infested(&mut self) {
if let Some(p) = self.player.as_ref() {
let at = [
p.position.x,
p.position.y + f64::from(p.dimensions.height) / 2.0,
p.position.z,
];
let look = super::view_vector(self.yaw, self.pitch);
self.infested_hurt(at, look, None);
}
}
/// A living mob's own random (`Entity.random`), else the world's.
fn bearer_random(&mut self, bearer: Option<u32>) -> &mut LegacyRandom {
match bearer.and_then(|id| self.mobs.iter().position(|m| m.id == id)) {
Some(i) => &mut self.mobs[i].random,
None => &mut self.world.random,
}
}
/// `InfestedMobEffect.onMobHurt`: a 10% chance of one or two silverfish at
/// `at` (half the bearer's height up), thrown along its look direction.
/// The draws are the bearer's (`mob.getRandom()`): `bearer` is the mob,
/// none for the player (the world's random).
pub(super) fn infested_hurt(&mut self, at: [f64; 3], look: [f64; 3], bearer: Option<u32>) {
if self.bearer_random(bearer).next_float() > 0.1 {
return;
}
let count = 1 + self.bearer_random(bearer).next_int_bound(2).unwrap_or(0);
for _ in 0..count {
// Mth.randomBetween(-PI / 2, PI / 2), then the look direction
// scaled by (0.3, 0.45, 0.3) and turned about the y axis.
let half = std::f32::consts::FRAC_PI_2;
let angle = self.bearer_random(bearer).next_float() * (2.0 * half) - half;
let (sin, cos) = angle.sin_cos();
let (x, z) = (look[0] * 0.3, look[2] * 0.3);
let velocity = Vec3::new(
f64::from(x as f32 * cos + z as f32 * sin),
look[1] * 0.3 * 1.5,
f64::from(-x as f32 * sin + z as f32 * cos),
);
if let Some(id) = self.spawn_mob_for("minecraft:silverfish", at, false, true)
&& let Some(m) = self.mobs.iter_mut().find(|m| m.id == id)
{
m.velocity = velocity;
}
self.audio
.play("minecraft:entity.silverfish.hurt", Some(at), 1.0, 1.0);
}
}
/// A splash potion, lingering potion or experience bottle in flight (the
/// player's throws and dispensers both end up here).
pub(super) fn launch_thrown(
@@ -488,12 +635,34 @@ impl Game<'_> {
return;
};
if player.health.dead_or_dying() {
// LivingEntity.die → removeAllEffects.
// LivingEntity.die → removeAllEffects. Vanilla runs the effects
// that act on death (`onMobRemoved`) when the dead player is
// removed, up to 20 ticks later; here it is as the effects go.
let w = f64::from(player.dimensions.width) / 2.0;
let bounds = Aabb::new(
Vec3::new(
player.position.x - w,
player.position.y,
player.position.z - w,
),
Vec3::new(
player.position.x + w,
player.position.y + f64::from(player.dimensions.height),
player.position.z + w,
),
);
let ids: Vec<&'static str> = self.potions.effects.list.iter().map(|e| e.id).collect();
self.potions.effects.clear();
if !ids.is_empty() {
self.effects_on_death(&ids, bounds, true, None, None);
}
}
// The omens act from inside the effect tick (BadOmenMobEffect,
// RaidOmenMobEffect).
let absorption_empty = player.health.absorption() <= 0.0;
let absorption_empty = self
.player
.as_ref()
.is_some_and(|p| p.health.absorption() <= 0.0);
let mut effects = std::mem::take(&mut self.potions.effects);
let (actions, raid_omen_added) =
effects.tick_with(&mut |e| self.omen_tick(e), absorption_empty);
+7
View File
@@ -926,6 +926,10 @@ pub enum MobEvent {
slot: usize,
stack: Option<crate::items::ItemStack>,
},
/// `InfestedMobEffect.onMobHurt`: the mob was hurt while infested.
Infested {
mob: u32,
},
/// `OfferFlowerGoal.stop`: the item goes to the target's antenna (saddle)
/// slot if it is empty, as a guaranteed drop.
Gift {
@@ -1929,6 +1933,9 @@ impl Mob {
}
}
}
if self.has_effect("minecraft:infested") {
self.outbox.push(MobEvent::Infested { mob: self.id });
}
// Squid.hurtServer: a squid hurt after a living entity hit it squirts ink.
if matches!(self.species(), Species::Squid | Species::GlowSquid)
&& self.ai.last_hurt_by.is_some()