mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 17:19:10 -10:00
commit
65b0cb2521
5 files changed
+246
-20
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user