Keep drowning bubble count informational

This commit is contained in:
arismoko committed 2026-11-02 11:11:19 +00:00
1 parent 5b433baaf9
commit bd7e5670fe
8 files changed
+32 -113

No files matched your search

+7 -13
View File
@@ -58,15 +58,16 @@ pub fn breathe(entity: u32, kind: &str, vanilla: Breath) -> Breath {
/// An entity that can't breathe and has no air (`LivingDrownEvent`, posted
/// from `LivingEntity.baseTick`), with vanilla's answers as the defaults: it
/// drowns at -20 air, for 2 damage and 8 bubbles. Asked on servers, for the
/// entities `native_living_breathe` is. API @API:native_living_drown@.
/// drowns at -20 air, for 2 damage and 8 bubbles. Bubble count is read-only;
/// changes to the event field do not affect the vanilla entity event. Asked on
/// servers, for the entities `native_living_breathe` is. API @API:native_living_drown@.
#[inline(never)]
pub fn native_living_drown(
entity: u32,
kind: &str,
is_drowning: &mut bool,
damage_amount: &mut f32,
bubble_count: &mut i32,
bubble_count: i32,
cancel: &mut bool,
) {
crate::native_client_hooks::living_drown(
@@ -97,16 +98,9 @@ pub fn drown(
) -> Option<Drowning> {
let (next, drowned) = apply(*air, 300, breath);
*air = if drowned { *air - breath.consume } else { next };
let (mut drowning, mut damage, mut bubbles, mut cancel) = (drowned, 2.0, 8, false);
let (mut drowning, mut damage, mut cancel) = (drowned, 2.0, false);
if server && !breath.can_breathe && *air <= 0 {
native_living_drown(
entity,
kind,
&mut drowning,
&mut damage,
&mut bubbles,
&mut cancel,
);
native_living_drown(entity, kind, &mut drowning, &mut damage, 8, &mut cancel);
}
if cancel || !drowning {
return None;
@@ -114,7 +108,7 @@ pub fn drown(
*air = 0;
Some(Drowning {
damage: (damage > 0.0).then_some(damage),
bubbles: bubbles.max(0),
bubbles: 8,
})
}
+4 -14
View File
@@ -7514,22 +7514,12 @@ impl Game<'_> {
pitch,
);
}
MotionEvent::Drown(bubbles) => {
MotionEvent::Drown(_) => {
if let Some(host) = &self.lan {
if bubbles == 8 {
host.hub
.entity_event(host.host_entity, combat::DROWN_EVENT, feet);
} else {
combat::send_drown_particles(
host,
feet,
player_velocity,
bubbles,
&mut self.world.random,
);
}
host.hub
.entity_event(host.host_entity, combat::DROWN_EVENT, feet);
}
self.drown_particles(feet, player_velocity, bubbles);
self.drown_particles(feet, player_velocity);
}
MotionEvent::BubbleColumnParticle { bubble, position } => {
// ServerLevel.sendParticles: SPLASH (speed 1) or BUBBLE
+4 -42
View File
@@ -174,50 +174,12 @@ impl CombatTracker {
}
}
}
/// Send a mod-adjusted bubble count using the vanilla particle packet, since
/// entity event 67 always renders exactly eight bubbles on the client.
pub(super) fn send_drown_particles(
host: &super::lan::LanHost,
at: [f64; 3],
velocity: [f64; 3],
count: i32,
random: &mut horizon_core::random::LegacyRandom,
) {
use horizon_core::random::RandomSource;
use horizon_protocol::{
entity_data::Particle,
packets::play::{self, Clientbound},
};
let Some(id) = host
.hub
.registry_id("minecraft:particle_type", "minecraft:bubble")
else {
return;
};
for _ in 0..count {
let pos = at.map(|v| v + random.next_double() - random.next_double());
let packet = Clientbound::LevelParticles(play::LevelParticles {
particle: Particle { id, data: None },
override_limiter: false,
always_show: false,
pos,
dist: velocity.map(|v| v as f32),
max_speed: [1.0; 3],
count: 0,
randomization: 0,
});
for guest in host.hub.guests.iter().filter(|g| host.hub.here(g)) {
guest.send(&packet);
}
}
}
impl Game<'_> {
/// `LivingEntity.makeDrownParticles`: bubbles around the entity,
/// `LivingEntity.makeDrownParticles`: eight bubbles around the entity,
/// moving with it.
pub(super) fn drown_particles(&mut self, at: [f64; 3], velocity: [f64; 3], count: i32) {
pub(super) fn drown_particles(&mut self, at: [f64; 3], velocity: [f64; 3]) {
let bubble = ParticleOptions::simple(ParticleType::Bubble);
for _ in 0..count {
for _ in 0..8 {
let mut triangle = || {
let r = self.particles.random();
r.next_double() - r.next_double()
@@ -242,7 +204,7 @@ impl Game<'_> {
})
};
if let Some((at, v)) = body {
self.drown_particles(at, v, 8);
self.drown_particles(at, v);
}
}
/// The display name of a mob: its custom name or its type's name.
+3 -20
View File
@@ -1744,31 +1744,14 @@ impl Game<'_> {
}
MobEvent::SquidCycle { mob, at } => self.lan_entity_event(mob as i32, 19, at),
MobEvent::RabbitHop { mob, at } => self.lan_entity_event(mob as i32, 1, at),
MobEvent::Drown { mob, at, bubbles } => {
MobEvent::Drown { mob, at, .. } => {
self.lan_entity_event(mob as i32, super::combat::DROWN_EVENT, at);
let v = self
.mobs
.iter()
.find(|m| m.id == mob)
.map_or([0.0; 3], |m| [m.velocity.x, m.velocity.y, m.velocity.z]);
if bubbles == 8 {
self.lan_entity_event(mob as i32, super::combat::DROWN_EVENT, at);
} else {
for _ in 0..bubbles {
let random = self.particles.random();
let pos = at.map(|x| x + random.next_double() - random.next_double());
self.lan_send_particles(super::lan_particles::QueuedParticle {
dimension: self.dim.id(),
options: ParticleOptions::simple(ParticleType::Bubble),
state: None,
at: pos,
spread: v,
speed: 1.0,
count: 0,
randomization: 0,
});
}
}
self.drown_particles(at, v, bubbles);
self.drown_particles(at, v);
}
MobEvent::SunProtectionWear { mob } => self.wear_sun_protection(mob),
MobEvent::ArmorHurt { mob, amount, kind } => self.hurt_mob_armor(mob, amount, kind),
+5 -15
View File
@@ -4289,22 +4289,12 @@ impl Game<'_> {
}
// LivingEntity.baseTick's entity event 67, to every client
// around, the guest's own included; this window draws it too.
MotionEvent::Drown(bubbles) => {
if bubbles == 8 {
if let Some(g) = host.hub.guest(conn) {
host.hub
.entity_event(g.entity_id, super::combat::DROWN_EVENT, at);
}
} else {
super::combat::send_drown_particles(
host,
at,
[0.0; 3],
bubbles,
&mut self.world.random,
);
MotionEvent::Drown(_) => {
if let Some(g) = host.hub.guest(conn) {
host.hub
.entity_event(g.entity_id, super::combat::DROWN_EVENT, at);
}
self.drown_particles(at, [0.0; 3], bubbles);
self.drown_particles(at, [0.0; 3]);
}
// HoneyBlock.maybeDoSlideEffects: the sound reaches the
// others (Player.playSound excludes the player, whose client
+5 -5
View File
@@ -3134,8 +3134,9 @@ pub trait Hooks: Send + Sync {
}
/// An entity that can't breathe and has no air (`LivingDrownEvent`, posted
/// from `LivingEntity.baseTick`), with vanilla's answers as the defaults: it
/// drowns at -20 air, for 2 damage and 8 bubbles. Asked on servers, for the
/// entities `native_living_breathe` is. API @API:native_living_drown@.
/// drowns at -20 air, for 2 damage and 8 bubbles. Bubble count is read-only;
/// changes to the event field do not affect the vanilla entity event. Asked on
/// servers, for the entities `native_living_breathe` is. API @API:native_living_drown@.
///
/// Return `Flow::Cancel` to take it: this sets `cancel`, and later mods are skipped.
fn living_drown(&self, event: &mut LivingDrownEvent<'_>) -> Flow {
@@ -10167,7 +10168,7 @@ pub fn dispatch_living_drown(
kind: &str,
is_drowning: &mut bool,
damage_amount: &mut f32,
bubble_count: &mut i32,
bubble_count: i32,
cancel: &mut bool,
) {
if *cancel {
@@ -10178,7 +10179,7 @@ pub fn dispatch_living_drown(
kind,
is_drowning: *is_drowning,
damage_amount: *damage_amount,
bubble_count: *bubble_count,
bubble_count,
};
for mod_hook in mods {
if matches!(mod_hook.living_drown(&mut event), Flow::Cancel) {
@@ -10188,7 +10189,6 @@ pub fn dispatch_living_drown(
}
*is_drowning = event.is_drowning;
*damage_amount = event.damage_amount;
*bubble_count = event.bubble_count;
}
/// What [`Hooks::living_fall`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
@@ -8,7 +8,7 @@ pub struct NativeClientHooks {
pub menu_slot_rules: fn(&str, usize, &ItemStack, &mut bool, &mut u32),
pub living_heal: fn(u32, &mut f32, &mut bool),
pub living_breathe: fn(u32, &str, &mut bool, &mut i32, &mut i32),
pub living_drown: fn(u32, &str, &mut bool, &mut f32, &mut i32, &mut bool),
pub living_drown: fn(u32, &str, &mut bool, &mut f32, i32, &mut bool),
pub living_fall: fn(u32, &str, &mut f64, &mut f32, &mut bool),
pub living_jump: fn(u32, &str),
pub ingredient: fn(&str, &Map<String, Value>, &mut Vec<String>),
@@ -56,7 +56,7 @@ pub(crate) fn living_drown(
kind: &str,
is_drowning: &mut bool,
damage_amount: &mut f32,
bubble_count: &mut i32,
bubble_count: i32,
cancel: &mut bool,
) {
if let Some(hooks) = HOOKS.get() {
@@ -11,7 +11,7 @@
}
}
],
"rust_summary": "a living entity drowning, with changeable drowning, damage and bubbles (`native_living_drown`).",
"doc_summary": "a living entity drowning (`native_living_drown`), with changeable drowning, damage and bubbles and cancellation. Bundles need \"hooks\": @API:native_living_drown@.",
"rust_summary": "a living entity drowning, with changeable drowning and damage and a read-only bubble count (`native_living_drown`).",
"doc_summary": "a living entity drowning (`native_living_drown`), with changeable drowning and damage, a read-only bubble count and cancellation. Bundles need \"hooks\": @API:native_living_drown@.",
"comments": ["crates/horizon-client/src/breathing.rs"]
}