Summoned splash and lingering potions are projectiles, not inert entities

ThrowableItemProjectile.getDefaultItem/readAdditionalSaveData and
AbstractThrownPotion.getDefaultGravity: /summon minecraft:splash_potion makes a
ThrownSplashPotion that falls with gravity 0.05, carries Motion, flies as its
Item (the default splash potion when the summon names none) and splashes with
that item's potion_contents; a lingering one leaves its cloud.

Horizon loaded both potion types as an inert "other" entity kept as its
compound - nothing ticked it, its data had no Motion, no blast could push it and
nothing splashed - and ProjectileKind::Potion carried one baked effect (the
witch's) instead of the stack, so the kind now holds the item it flies as and a
landed potion goes through the same splash() a thrown one does; the witch builds
the stack vanilla's Witch.performRangedAttack makes. ThrowableProjectile's air
drag is vanilla's 0.99F float widened to double, not 0.99, so a thrown item's
position matches the server's (the splash potion's Pos and Motion after a summon
now match to the last digit).

Fixes #6642
This commit is contained in:
MSedgeMC committed 2026-11-02 05:08:44 +00:00
1 parent 1c2ed6b613
commit ab0afa3174
8 files changed
+334 -158

No files matched your search

+100 -21
View File
@@ -2384,26 +2384,37 @@ fn put_projectile(tag: &mut Tag, p: &Projectile, owner: Option<[i32; 4]>) {
/// The thrown item projectiles Horizon saves: each entity type's id is also
/// the item it saves when none was loaded (`getDefaultItem`).
const THROWABLES: [(ProjectileKind, &str); 3] = [
(ProjectileKind::Snowball, "minecraft:snowball"),
(ProjectileKind::Egg, "minecraft:egg"),
(ProjectileKind::EnderPearl, "minecraft:ender_pearl"),
const THROWABLE_ITEMS: [&str; 5] = [
"minecraft:snowball",
"minecraft:egg",
"minecraft:ender_pearl",
"minecraft:splash_potion",
"minecraft:lingering_potion",
];
fn throwable_item(kind: &ProjectileKind) -> Option<&'static str> {
if let ProjectileKind::Thrown(t) = kind {
return Some(t.item);
/// The item a throwable flies as: its stack (a potion's), the mod type's
/// item, or the item its entity type is named after.
fn throwable_item(kind: &ProjectileKind) -> Option<&str> {
match kind {
ProjectileKind::Thrown(t) => Some(t.item),
ProjectileKind::Potion(stack) => Some(&stack.item),
_ => THROWABLE_ITEMS.iter().find(|id| **id == kind.id()).copied(),
}
THROWABLES
.iter()
.find(|(k, _)| k == kind)
.map(|&(_, id)| id)
}
/// The thrown kind an entity id names: vanilla's, or a content mod's type.
/// A potion flies as the item its entity type names, which is the item a
/// summoned one has when it names none (`getDefaultItem`).
pub fn throwable_kind(id: &str) -> Option<ProjectileKind> {
match THROWABLES.iter().find(|(_, i)| *i == id) {
Some((kind, _)) => Some(kind.clone()),
None => crate::projectiles::thrown_type(id).map(ProjectileKind::Thrown),
if !THROWABLE_ITEMS.contains(&id) {
return crate::projectiles::thrown_type(id).map(ProjectileKind::Thrown);
}
match id {
"minecraft:snowball" => Some(ProjectileKind::Snowball),
"minecraft:egg" => Some(ProjectileKind::Egg),
"minecraft:ender_pearl" => Some(ProjectileKind::EnderPearl),
_ => Some(ProjectileKind::Potion(Box::new(
crate::items::ItemStack::new(id, 1),
))),
}
}
@@ -2441,13 +2452,14 @@ pub fn throwable_to_nbt(
if p.no_gravity {
put(&mut tag, "NoGravity", Tag::Byte(1));
}
// the loaded item stays (a blue egg's, a renamed one's).
// the loaded item stays (a blue egg's, a renamed one's, a potion's
// effects); when there is none, `getItem`'s default is written.
if tag.get("Item").is_none() {
put(
&mut tag,
"Item",
item_to_nbt(&crate::items::ItemStack::new(item, 1)),
);
let stack = match &p.kind {
ProjectileKind::Potion(stack) => (**stack).clone(),
_ => crate::items::ItemStack::new(item, 1),
};
put(&mut tag, "Item", item_to_nbt(&stack));
}
Some(tag)
}
@@ -2455,7 +2467,14 @@ pub fn throwable_to_nbt(
/// `ThrowableItemProjectile.readAdditionalSaveData` for the kinds
/// [`throwable_to_nbt`] writes; the owner is the player when it is theirs.
pub fn throwable_from_nbt(tag: &Tag, player: Option<[i32; 4]>) -> Option<Projectile> {
let kind = throwable_kind(&kind(tag)?)?;
let mut kind = throwable_kind(&kind(tag)?)?;
// `readAdditionalSaveData` sets the `Item` it flies as, potion effects
// and all; a tag without one keeps `getItem`'s default.
if let ProjectileKind::Potion(stack) = &mut kind
&& let Some(item) = tag.get("Item").and_then(item_from_nbt)
{
**stack = item;
}
let owner = owner_uuid(tag);
let from_player = player.is_some() && owner == player;
let mut p = Projectile::new(kind, position(tag)?, from_player.then_some(Who::Player));
@@ -4742,6 +4761,66 @@ mod fireball_nbt_tests {
assert!(back.get("Item").is_none());
}
#[test]
fn a_summoned_potion_flies_as_the_item_its_compound_names() {
// `ThrowableItemProjectile.getDefaultItem`: the item its entity type is
// named after, so a bare summon is a splash potion with no effects and
// the chunk saves it (ThrowableProjectile is not transient).
for id in ["minecraft:splash_potion", "minecraft:lingering_potion"] {
let bare = Tag::compound([
("id", Tag::String(id.into())),
("Pos", doubles(&[24.5, 216.0, 24.5])),
]);
let p = throwable_from_nbt(&bare, None).unwrap();
assert_eq!(p.kind.id(), id);
assert_eq!(
p.kind,
ProjectileKind::Potion(Box::new(crate::items::ItemStack::new(id, 1)))
);
assert!(saved_with_chunk(&p));
let tag = throwable_to_nbt(&p, [1; 4], None, None).unwrap();
let item = tag.get("Item").and_then(item_from_nbt).unwrap();
assert_eq!((&*item.item, item.count), (id, 1));
assert_eq!(item.potion(), None);
}
// `readAdditionalSaveData`: the `Item` it flies as, `potion_contents`
// and all, so what it splashes is the potion's.
let harming = Tag::compound([
("id", Tag::String("minecraft:splash_potion".into())),
("Pos", doubles(&[24.5, 216.0, 24.5])),
(
"Item",
item_to_nbt(
&crate::items::ItemStack::new("minecraft:splash_potion", 1)
.with_potion("minecraft:harming"),
),
),
]);
let p = throwable_from_nbt(&harming, None).unwrap();
let ProjectileKind::Potion(stack) = &p.kind else {
panic!("a potion");
};
assert_eq!(stack.potion(), Some("minecraft:harming"));
// The stack stays in the tag it was loaded from.
let tag = throwable_to_nbt(&p, [1; 4], None, Some(&harming)).unwrap();
let item = tag.get("Item").and_then(item_from_nbt).unwrap();
assert_eq!(item.potion(), Some("minecraft:harming"));
// A lingering potion is its own entity type.
let lingering = Tag::compound([
("id", Tag::String("minecraft:lingering_potion".into())),
("Pos", doubles(&[0.0, 64.0, 0.0])),
(
"Item",
item_to_nbt(
&crate::items::ItemStack::new("minecraft:lingering_potion", 1)
.with_potion("minecraft:poison"),
),
),
]);
let p = throwable_from_nbt(&lingering, None).unwrap();
assert_eq!(p.kind.id(), "minecraft:lingering_potion");
}
#[test]
fn thrown_item_projectiles_round_trip_their_flight_item_and_owner() {
let player = Some([5, 6, 7, 8]);
+27 -78
View File
@@ -4593,84 +4593,33 @@ impl Game<'_> {
self.combat.explosion_in_water = false;
result?;
}
(
ProjectileKind::Potion {
effect,
ticks,
amplifier,
..
},
_,
) => {
// AbstractThrownPotion.applySplash: within 4 blocks, scaled by distance.
if let Some(pv) = self.player_view() {
let d2 = (pv.position.x - at.x).powi(2)
+ (pv.position.y - at.y).powi(2)
+ (pv.position.z - at.z).powi(2);
if d2 < 16.0 && !self.creative {
let scale = 1.0 - d2.sqrt() / 4.0;
let t = (f64::from(*ticks) * scale + 0.5) as i32;
match *effect {
"minecraft:instant_damage" => {
// indirectMagic(potion, owner), built where the
// hurt happens: the thrower, or the potion
// itself when it has none. A status effect
// deals no damage, so it records no source.
let owner = self
.combat
.projectile_owner
.filter(|id| self.mob_display_name(*id).is_some());
match owner {
Some(owner) => {
self.note_attacker("minecraft:indirect_magic", owner)
}
None => self.note_direct_entity(
"minecraft:indirect_magic",
Text::translate("entity.minecraft.splash_potion", &[]),
),
}
self.hurt_player_by(
"minecraft:indirect_magic",
6.0 * (1 << *amplifier) as f32 * scale as f32,
)
}
e if t > 20 => self.give_player_effect(e, t, *amplifier),
_ => {}
}
}
}
// A witch's healing potions reach the raiders around the splash.
if matches!(
*effect,
"minecraft:instant_health" | "minecraft:regeneration"
) {
let center = at;
for m in &mut self.mobs {
let d2 = m.position.distance_squared(center);
if !m.alive()
|| d2 >= 16.0
|| !horizon_client::mobs::raider::is_raider(m.species())
{
continue;
}
let scale = 1.0 - d2.sqrt() / 4.0;
if *effect == "minecraft:instant_health" {
let heal = (4 << *amplifier) as f64 * scale;
m.health = (m.health + heal as f32).min(m.attrs.max_health);
} else {
let t = (f64::from(*ticks) * scale + 0.5) as i32;
if t > 20 {
m.add_effect(effect, t, *amplifier);
}
}
}
}
self.audio.play(
"minecraft:entity.splash_potion.break",
Some(atp),
1.0,
self.world.random.next_float() * 0.1 + 0.9,
);
// AbstractThrownPotion.onHit, then onHitAsPotion from the stack it
// flies as: the splash happens where the hit was, a lingering
// potion's cloud sits at the mob it hit (ThrownLingeringPotion),
// and the potion that lands names the hurt it deals.
(ProjectileKind::Potion(stack), hit) => {
let block = match hit {
Hit::Block { pos, face, .. } => Some((
pos,
Direction::approximate_nearest(Vec3::new(
f64::from(face[0]),
f64::from(face[1]),
f64::from(face[2]),
)),
)),
_ => None,
};
let mob = match hit {
Hit::Entity {
who: Who::Player, ..
} => self.player.as_ref().map(|pv| pv.position),
Hit::Entity {
who: Who::Mob(id), ..
} => self.mobs.iter().find(|m| m.id == id).map(|m| m.position),
_ => None,
};
let owner = self.potion_owner(p.owner);
self.splash(stack, at, mob, block, p.age, &owner);
}
(ProjectileKind::LlamaSpit, Hit::Entity { who, .. }) => match who {
Who::Player => {
@@ -623,6 +623,17 @@ impl Game<'_> {
K::WitherSkull { dangerous: true } => {
e.meta.push((8, serializer::BOOLEAN, Value::Bool(true)));
}
// ThrowableItemProjectile.DATA_ITEM_STACK: the potion's
// `potion_contents`, whose effects the guest's splash and
// whose colour its drawing need. A plain splash potion is the
// client's default and is not sent.
K::Potion(stack) if stack.extra.is_some() || stack.count != 1 => {
e.meta.push((
8,
serializer::ITEM_STACK,
Value::Item(hub.stack(&Some((**stack).clone()))),
));
}
_ => {}
}
out.push(e);
+68 -16
View File
@@ -117,6 +117,21 @@ impl PotionCloud {
ParticleOptions::colored(ParticleType::EntityEffect, [c(16), c(8), c(0), 1.0])
}
}
/// `Projectile.getOwner` of a thrown potion: what its `indirectMagic` damage
/// is named after and who it counts against. Nothing for a dispenser's or a
/// summoned potion nobody owns, the potion itself then naming the death
/// message.
#[derive(Clone, Debug, Default)]
pub(super) struct PotionOwner {
/// The thrower's name, when it has one.
name: Option<Text>,
/// The mob that threw it (a witch): the hurt is its kill credit, as
/// `indirectMagic(potion, owner)` gives it. A player's throw is the
/// potion's own and only `name`s the message.
attacker: Option<u32>,
/// A player threw it: the water damage `indirectMagic` deals is theirs.
player: bool,
}
/// A mob's effect ids carry the `minecraft:` namespace; a mod's has its own.
pub(super) fn mob_effect_id(id: &str) -> Option<&'static str> {
brewing::effect(id).map(|e| brewing::namespaced(e.id))
@@ -1165,7 +1180,13 @@ impl Game<'_> {
};
let thrower = owner.is_some().then_some(Who::Player);
self.projectile_event("minecraft:projectile_land", thrower, heard);
self.splash(&stack, at, mob, block, age, owner);
// A potion a dispenser threw has nobody to name.
let owner = PotionOwner {
player: owner.is_some(),
name: owner,
attacker: None,
};
self.splash(&stack, at, mob, block, age, &owner);
}
}
/// `AreaEffectCloud.tick` for the lingering potions' clouds: the client
@@ -1301,6 +1322,12 @@ impl Game<'_> {
at: [f64; 3],
source: &Text,
) {
// `indirectMagic(cloud, owner)` names the cloud in the death message
// (it gives no kill credit).
let cloud_owner = PotionOwner {
name: Some(source.clone()),
..Default::default()
};
// MobEffectInstance.withScaledDuration: floor(duration * scale), at least 1; an infinite
// effect stays infinite.
let scaled = |duration: i32| {
@@ -1318,7 +1345,7 @@ impl Game<'_> {
};
if e.instant {
if let Some(a) = horizon_client::effects::instant(id, *amp, 0.5) {
g.apply_indirect_effect_action(a, source);
g.apply_indirect_effect_action(a, &cloud_owner);
}
} else {
let mut instance = EffectInstance::new(e.id, scaled(*duration), *amp);
@@ -1476,11 +1503,22 @@ impl Game<'_> {
}
}
/// `HealOrHarmMobEffect.applyInstantaneousEffect` with a source (a
/// potion or a cloud): indirectMagic, not plain magic.
fn apply_indirect_effect_action(&mut self, a: EffectAction, source: &Text) {
/// potion or a cloud): indirectMagic, not plain magic, credited to the
/// mob that threw it when there is one.
fn apply_indirect_effect_action(&mut self, a: EffectAction, owner: &PotionOwner) {
let a = match a {
EffectAction::Hurt("minecraft:magic", amount) => {
self.note_direct_entity("minecraft:indirect_magic", source.clone());
match (owner.attacker, &owner.name) {
(Some(id), Some(name)) => {
self.note_named_attacker("minecraft:indirect_magic", id, name.clone())
}
_ => {
let name = owner.name.clone().unwrap_or_else(|| {
Text::translate("entity.minecraft.splash_potion", &[])
});
self.note_direct_entity("minecraft:indirect_magic", name);
}
}
EffectAction::Hurt("minecraft:indirect_magic", amount)
}
a => a,
@@ -1493,7 +1531,7 @@ impl Game<'_> {
effects: &'static [(&'static str, i32, i32)],
at: Vec3,
age: u32,
source: &Text,
owner: &PotionOwner,
) {
if let Some(p) = self.player.as_ref() {
let w = f64::from(p.dimensions.width) / 2.0;
@@ -1511,7 +1549,7 @@ impl Game<'_> {
let instant = brewing::effect(id).is_some_and(|e| e.instant);
if instant {
if let Some(a) = horizon_client::effects::instant(id, *amp, intensity) {
self.apply_indirect_effect_action(a, source);
self.apply_indirect_effect_action(a, owner);
}
} else {
let d = (intensity * f64::from(*duration) + 0.5) as i32;
@@ -1525,18 +1563,36 @@ impl Game<'_> {
}
}
}
/// `Projectile.getOwner` of a thrown potion, as [`splash`](Self::splash)
/// names it: a player's, a mob's (a witch keeps the kill credit), or
/// nobody's (a dispenser's, a summoned potion's).
pub(super) fn potion_owner(&self, owner: Option<Who>) -> PotionOwner {
match owner {
Some(Who::Player) => PotionOwner {
name: Some(self.player_display_text()),
attacker: None,
player: true,
},
Some(Who::Mob(id)) => PotionOwner {
name: self.mob_display_name(id),
attacker: Some(id),
player: false,
},
None => PotionOwner::default(),
}
}
/// `AbstractThrownPotion.onHit` with `ThrownSplashPotion.onHitAsPotion`, or
/// `ThrownLingeringPotion.onHitAsPotion` for a lingering potion, read from
/// the stack's `potion_contents`; `mob` is where it hit a mob, `hit` the
/// block and face it hit, `age` its age.
fn splash(
pub(super) fn splash(
&mut self,
stack: &ItemStack,
at: Vec3,
mob: Option<Vec3>,
hit: Option<([i32; 3], Direction)>,
age: u32,
owner: Option<Text>,
owner: &PotionOwner,
) {
let block = [at.x, at.y, at.z].map(|v| v.floor() as i32);
if &*stack.item == "minecraft:experience_bottle" {
@@ -1554,7 +1610,7 @@ impl Game<'_> {
if let Some((cell, face)) = hit {
self.potion_hit_block(stack, cell, face);
}
self.affect_entities_around(stack, at, owner.is_some());
self.affect_entities_around(stack, at, owner.player);
let lingering = &*stack.item == "minecraft:lingering_potion";
// AbstractThrownPotion.onHit: level event 2007 and sound 1054 for a
// potion with instant effects, 2002 and 1053 otherwise.
@@ -1618,7 +1674,7 @@ impl Game<'_> {
radius_per_tick: -3.0 / 600.0,
color,
victims: Vec::new(),
source: owner.unwrap_or_else(|| {
source: owner.name.clone().unwrap_or_else(|| {
Text::translate("entity.minecraft.area_effect_cloud", &[])
}),
}),
@@ -1628,11 +1684,7 @@ impl Game<'_> {
}
// Every player in range (`ThrownSplashPotion.applySplash` over
// `LivingEntity`s, the players among them).
// Instant damage is indirectMagic(potion, owner), which the death
// message names by the owner, else by the potion.
let source =
owner.unwrap_or_else(|| Text::translate("entity.minecraft.splash_potion", &[]));
self.for_each_player(|g| g.splash_on_player(def.effects, at, age, &source));
self.for_each_player(|g| g.splash_on_player(def.effects, at, age, owner));
// Mobs take the instant effects (undead invert healing and harming).
let undead = &self.stations().enchantments.entity_tags;
let mut hits = Vec::new();
@@ -918,11 +918,13 @@ mod tests {
#[test]
fn a_thrown_item_falls_and_drags_like_a_throwable_projectile() {
// ThrowableProjectile.tick: gravity 0.03, then the air drag 0.99.
// ThrowableProjectile.tick: gravity 0.03, then the air drag 0.99F
// (`getAirDrag`, a float widened to the double it scales by).
let drag = f64::from(0.99_f32);
let mut p = thrown(ProjectileKind::Egg);
remote_projectile_tick(&mut p, &Empty);
assert!((p.velocity.x - 1.5 * 0.99).abs() < 1e-9, "{p:?}");
assert!((p.velocity.y - (-0.03 * 0.99)).abs() < 1e-9, "{p:?}");
assert!((p.velocity.x - 1.5 * drag).abs() < 1e-9, "{p:?}");
assert!((p.velocity.y - (-0.03 * drag)).abs() < 1e-9, "{p:?}");
// A wind charge keeps its speed: no gravity and no drag
// (AbstractWindCharge: acceleration 0, inertia 1).
let mut p = thrown(ProjectileKind::WindCharge);
+12 -13
View File
@@ -204,30 +204,29 @@ fn witch_attack(m: &mut Mob, who: Who, _power: f32, cx: &mut Cx) {
Who::Player => cx.w.player.as_ref().is_some_and(|p| p.has_effect(effect)),
Who::Mob(id) => cx.w.seen(id).is_some_and(|s| s.has_effect(effect)),
};
let (effect, ticks, amplifier) = if heals {
// The potion it throws: `PotionContents.createItemStack(Items.SPLASH_POTION,
// potion)` for the one `Witch.performRangedAttack` picks.
let potion = if heals {
m.ai.target = None;
if health <= 4.0 {
("minecraft:instant_health", 1, 0)
"minecraft:healing"
} else {
("minecraft:regeneration", 900, 0)
"minecraft:regeneration"
}
} else if dist >= 8.0 && !has("slowness") {
("minecraft:slowness", 1800, 0)
"minecraft:slowness"
} else if health >= 8.0 && !has("poison") {
("minecraft:poison", 900, 0)
"minecraft:poison"
} else if dist <= 3.0 && !has("weakness") && m.random.next_float() < 0.25 {
("minecraft:weakness", 1800, 0)
"minecraft:weakness"
} else {
("minecraft:instant_damage", 1, 0)
"minecraft:harming"
};
let start = Vec3::new(m.position.x, m.eye_y() - 0.1, m.position.z);
let mut p = Projectile::new(
ProjectileKind::Potion {
effect,
ticks,
amplifier,
color: 0,
},
ProjectileKind::Potion(Box::new(
crate::items::ItemStack::new("minecraft:splash_potion", 1).with_potion(potion),
)),
start,
Some(Who::Mob(m.id)),
);
+38 -27
View File
@@ -105,13 +105,11 @@ pub enum ProjectileKind {
WitherSkull {
dangerous: bool,
},
/// Splash potion thrown by witches: effect id, duration.
Potion {
effect: &'static str,
ticks: i32,
amplifier: i32,
color: u32,
},
/// `ThrownSplashPotion` or `ThrownLingeringPotion`: the potion item it
/// flies as (`ThrowableItemProjectile.getItem`). What it splashes is that
/// stack's `potion_contents`, so a witch's potion and a summoned one are
/// the same thing; its entity type is the stack's item.
Potion(Box<crate::items::ItemStack>),
/// a content mod's type.
Thrown(&'static ThrownType),
}
@@ -137,7 +135,7 @@ impl ProjectileKind {
| Self::Egg
| Self::EnderPearl
| Self::LlamaSpit
| Self::Potion { .. }
| Self::Potion(_)
| Self::Thrown(_) => 0.25,
Self::SmallFireball
| Self::WindCharge
@@ -162,11 +160,29 @@ impl ProjectileKind {
Self::BreezeWindCharge => "minecraft:breeze_wind_charge",
Self::ShulkerBullet { .. } => "minecraft:shulker_bullet",
Self::WitherSkull { .. } => "minecraft:wither_skull",
Self::Potion { .. } => "minecraft:splash_potion",
// `ThrownSplashPotion`/`ThrownLingeringPotion`: the item names it.
Self::Potion(stack) => potion_entity_type(&stack.item),
Self::Thrown(t) => t.id,
}
}
}
/// The entity type a thrown potion's item names: `ThrownSplashPotion` for a
/// splash potion, `ThrownLingeringPotion` for a lingering one.
fn potion_entity_type(item: &str) -> &'static str {
if item == "minecraft:lingering_potion" {
"minecraft:lingering_potion"
} else {
"minecraft:splash_potion"
}
}
/// `ThrownItemRenderer`'s sprite for a thrown potion.
fn potion_sprite(item: &str) -> &'static str {
if item == "minecraft:lingering_potion" {
"item/lingering_potion"
} else {
"item/splash_potion"
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Projectile {
/// The entity id a server tracks it by (0 until it is tracked).
@@ -438,7 +454,7 @@ impl Projectile {
| ProjectileKind::LargeFireball { .. }
| ProjectileKind::WitherSkull { .. } => 0.0,
ProjectileKind::LlamaSpit => 0.06,
ProjectileKind::Potion { .. } => 0.05,
ProjectileKind::Potion(_) => 0.05,
_ => 0.03,
}
}
@@ -839,9 +855,11 @@ impl Projectile {
}
}
_ => {
// ThrowableProjectile: gravity, then inertia.
// ThrowableProjectile: gravity, then inertia. Its drag is a
// float in vanilla (0.8F in water, `getAirDrag` 0.99F), so
// widens to the double that scales the motion.
self.velocity.y -= self.gravity();
let inertia = if water { 0.8 } else { 0.99 };
let inertia = f64::from(if water { 0.8_f32 } else { 0.99_f32 });
self.velocity = Vec3::new(
self.velocity.x * inertia,
self.velocity.y * inertia,
@@ -945,31 +963,24 @@ impl Projectile {
damage as f32
}
/// the item a thrown potion or a mod's type is drawn as with its model
/// (`ThrownItemRenderer`): the splash potion `Witch.performRangedAttack`
/// makes for the effect, or the type's item.
/// (`ThrownItemRenderer`): the potion stack itself, or the type's item.
pub fn drawn_item(&self) -> Option<crate::items::ItemStack> {
let effect = match self.kind {
ProjectileKind::Potion { effect, .. } => effect,
ProjectileKind::Thrown(t) => return Some(crate::items::ItemStack::new(t.item, 1)),
_ => return None,
};
let potion = match effect {
"minecraft:instant_health" => "minecraft:healing",
"minecraft:instant_damage" => "minecraft:harming",
other => other,
};
Some(crate::items::ItemStack::new("minecraft:splash_potion", 1).with_potion(potion))
match &self.kind {
ProjectileKind::Potion(stack) => Some((**stack).clone()),
ProjectileKind::Thrown(t) => Some(crate::items::ItemStack::new(t.item, 1)),
_ => None,
}
}
/// The item sprite drawn for thrown projectiles (ThrownItemRenderer).
pub fn item_sprite(&self) -> Option<&'static str> {
Some(match self.kind {
Some(match &self.kind {
ProjectileKind::Snowball => "item/snowball",
ProjectileKind::Egg => "item/egg",
ProjectileKind::EnderPearl => "item/ender_pearl",
ProjectileKind::SmallFireball | ProjectileKind::LargeFireball { .. } => {
"item/fire_charge"
}
ProjectileKind::Potion { .. } => "item/splash_potion",
ProjectileKind::Potion(stack) => potion_sprite(&stack.item),
ProjectileKind::LlamaSpit => "entity/llama/llama_spit",
// Drawn as `WindChargeModel` and `ShulkerBulletModel` by the game.
ProjectileKind::WindCharge
+73
View File
@@ -1578,6 +1578,78 @@ def experience_orb_stuck():
]
def summoned_potions():
"""`ThrowableItemProjectile.getItem`/`readAdditionalSaveData` and
`AbstractThrownPotion.getDefaultGravity`: a summoned splash or lingering
potion is the projectile its type is, not an entity nothing ticks. It
flies with the gravity 0.05 of its type (a bare one as a splash potion
with no effects), `Motion` is in its compound, and what it splashes on
landing are the effects of its `Item`'s `potion_contents` (#6642)."""
ops = [*setup('/kill @e[type=minecraft:splash_potion]',
'/kill @e[type=minecraft:lingering_potion]',
'/fill 0 196 0 20 200 20 minecraft:stone')]
# A bare summon flies as the default splash potion: no effects, and the
# tick's gravity and drag move it off where it was placed.
ops += [
cmd('/summon minecraft:splash_potion 4.5 206 4.5'),
cmd('/data get entity @e[type=minecraft:splash_potion,limit=1] Item'),
cmd('/data get entity @e[type=minecraft:splash_potion,limit=1] Motion'),
cmd('/data get entity @e[type=minecraft:splash_potion,limit=1] Pos'),
{'op': 'tick', 'n': 3},
cmd('/data get entity @e[type=minecraft:splash_potion,limit=1] Pos'),
cmd('/data get entity @e[type=minecraft:splash_potion,limit=1] Motion'),
{'op': 'tick', 'n': 30},
cmd('/execute if entity @e[type=minecraft:splash_potion]'),
]
# A harming potion landing on a pig hurts it for the potion's 6, and the
# potion is gone: ThrownSplashPotion.onHitAsPotion over the stack. The pig
# stands on the stone at y 201 (a summon at y 200 is inside it).
ops += [
*setup('/kill @e[type=!player]'),
{'op': 'tick', 'n': 25},
*setup('/summon minecraft:pig 4.5 201 4.5 {NoAI:1b,Silent:1b,PersistenceRequired:1b}'),
{'op': 'tick', 'n': 5},
cmd('/data get entity @e[type=minecraft:pig,limit=1] Health'),
*setup('/summon minecraft:splash_potion 4.5 206 4.5 '
'{Item:{id:"minecraft:splash_potion",count:1,components:'
'{"minecraft:potion_contents":{potion:"minecraft:harming"}}}}'),
{'op': 'tick', 'n': 40},
cmd('/data get entity @e[type=minecraft:pig,limit=1] Health'),
cmd('/execute if entity @e[type=minecraft:splash_potion]'),
]
# A lingering potion leaves its cloud where it lands, and the cloud puts
# the potion's poison on the pig standing in it
# (ThrownLingeringPotion.onHitAsPotion). The duration is left out of the
# check: how the cloud's age and the mob's effect tick interleave is not
# what this covers.
ops += [
*setup('/kill @e[type=!player]'),
{'op': 'tick', 'n': 25},
*setup('/summon minecraft:pig 8.5 201 8.5 '
'{Tags:["poisoned"],NoAI:1b,Silent:1b,PersistenceRequired:1b}',
'/summon minecraft:lingering_potion 8.5 206 8.5 '
'{Item:{id:"minecraft:lingering_potion",count:1,components:'
'{"minecraft:potion_contents":{potion:"minecraft:poison"}}}}'),
cmd('/data get entity @e[type=minecraft:lingering_potion,limit=1] Motion'),
{'op': 'tick', 'n': 40},
cmd('/execute if entity @e[type=minecraft:lingering_potion]'),
cmd('/execute if entity @e[tag=poisoned,limit=1,nbt='
'{active_effects:[{id:"minecraft:poison"}]}]'),
# A bare lingering potion has empty contents, so its cloud, ten blocks
# from the poison one, puts nothing on its own pig.
*setup('/summon minecraft:pig 18.5 201 18.5 '
'{Tags:["bare"],NoAI:1b,Silent:1b,PersistenceRequired:1b}',
'/summon minecraft:lingering_potion 18.5 206 18.5'),
{'op': 'tick', 'n': 40},
cmd('/execute if entity @e[tag=poisoned,limit=1,nbt='
'{active_effects:[{id:"minecraft:poison"}]}]'),
cmd('/execute if entity @e[tag=bare,limit=1,nbt='
'{active_effects:[{id:"minecraft:poison"}]}]'),
]
ops += [*setup('/kill @e[type=!player]'), {'op': 'tick', 'n': 25}]
return ops
def groups():
return [
('setup', group_setup()),
@@ -1612,4 +1684,5 @@ def groups():
('experience orb fall', experience_orb_fall()),
('piglin pickups', piglin_pickups()),
('experience orb stuck', experience_orb_stuck()),
('summoned potions', summoned_potions()),
]