mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Merge pull request #11033 from DUzzL/fix/10587-death-protection-item
Death protection: use the death_protection item and its effects, for players and mobs
This commit is contained in:
4 files changed
+284
-62
No files matched your search
@@ -12759,6 +12759,7 @@ impl Game<'_> {
|
||||
.collect()
|
||||
}
|
||||
fn tick_mobs(&mut self) -> Result<()> {
|
||||
self.sync_mob_death_protection();
|
||||
self.tick_creatures()?;
|
||||
if !self.entity_attachments.is_empty() {
|
||||
self.prune_entity_attachments();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! The totem of undying (`LivingEntity.checkTotemDeathProtection` with the
|
||||
//! totem's `death_protection` component, entity event 35 on the client).
|
||||
//! The totem of undying (`LivingEntity.checkTotemDeathProtection`: a hand's
|
||||
//! item carrying the `death_protection` component, whatever item it is, and
|
||||
//! entity event 35 on the client).
|
||||
use super::Game;
|
||||
use horizon_assets::item_model::DisplayContext;
|
||||
use horizon_client::{
|
||||
block_entity_render::vertex_light,
|
||||
effects::EffectInstance,
|
||||
items::ItemStack,
|
||||
mesher::Vertex,
|
||||
particles::{ParticleOptions, ParticleType},
|
||||
@@ -16,18 +16,64 @@ const TOTEM: &str = "minecraft:totem_of_undying";
|
||||
pub(super) const TOTEM_EVENT: i8 = 35;
|
||||
|
||||
impl Game<'_> {
|
||||
/// Whether either hand holds a totem, for the next hurt: the window's
|
||||
/// player once a tick, a server's player for each of its turns.
|
||||
/// Whether the stack has `DataComponents.DEATH_PROTECTION`, whatever
|
||||
/// item it is: the totem of undying is only one item that carries it.
|
||||
fn has_death_protection(&self, s: &ItemStack) -> bool {
|
||||
self.pack
|
||||
.item_renderer
|
||||
.models
|
||||
.components(&horizon_client::item_render::stack_view(s))
|
||||
.get("minecraft:death_protection")
|
||||
.is_some()
|
||||
}
|
||||
/// The `death_effects` of the stack's `minecraft:death_protection`.
|
||||
fn death_effects(&self, s: &ItemStack) -> Vec<horizon_client::items::ConsumeEffect> {
|
||||
death_effects_of(&self.pack.item_renderer.models, s)
|
||||
}
|
||||
/// What each armed mob's hands carry as `minecraft:death_protection`, for
|
||||
/// `Mob::use_death_protection` (the mob has no item data of its own): any
|
||||
/// item with the component counts, not only the totem.
|
||||
pub(super) fn sync_mob_death_protection(&mut self) {
|
||||
let models = &self.pack.item_renderer.models;
|
||||
let of = |id: Option<&std::sync::Arc<str>>, stack: Option<&ItemStack>| {
|
||||
let stack = match (stack, id) {
|
||||
(Some(s), _) => s.clone(),
|
||||
(None, Some(id)) => ItemStack::new(id, 1),
|
||||
(None, None) => return None,
|
||||
};
|
||||
models
|
||||
.components(&horizon_client::item_render::stack_view(&stack))
|
||||
.get("minecraft:death_protection")
|
||||
.map(horizon_client::items::death_effects)
|
||||
};
|
||||
for m in self.mobs.iter_mut().filter(|m| !m.removed) {
|
||||
if m.extra.main_hand.is_none() && m.extra.off_hand.is_none() {
|
||||
m.extra.hand_protection = None;
|
||||
continue;
|
||||
}
|
||||
m.extra.hand_protection = Some(horizon_client::mobs::HandProtection {
|
||||
main: of(m.extra.main_hand.as_ref(), m.extra.main_hand_stack()),
|
||||
off: of(m.extra.off_hand.as_ref(), None),
|
||||
});
|
||||
}
|
||||
}
|
||||
/// Whether either hand holds a death-protection item, for the next hurt:
|
||||
/// the window's player once a tick, a server's player for each of its
|
||||
/// turns.
|
||||
pub(super) fn update_totem_ready(&mut self) {
|
||||
let ready = self.held().is_some_and(|s| &*s.item == TOTEM)
|
||||
|| self.offhand.as_ref().is_some_and(|s| &*s.item == TOTEM);
|
||||
let ready = self.held().is_some_and(|s| self.has_death_protection(s))
|
||||
|| self
|
||||
.offhand
|
||||
.as_ref()
|
||||
.is_some_and(|s| self.has_death_protection(s));
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
p.health.totem_ready = ready;
|
||||
}
|
||||
}
|
||||
/// The server's half once a totem fired: use it up (main hand first),
|
||||
/// count it, set health to 1, then clear and apply its effects. Returns
|
||||
/// the player's body, for entity event 35, which the caller broadcasts.
|
||||
/// the player's body, for entity event 35, which the caller broadcasts,
|
||||
/// with the protector copy the activation animation shows.
|
||||
pub(super) fn apply_totem(&mut self) -> Option<Body> {
|
||||
if !self.player.as_ref()?.health.totem_used {
|
||||
return None;
|
||||
@@ -43,42 +89,43 @@ impl Game<'_> {
|
||||
at: [p.position.x, p.position.y, p.position.z],
|
||||
width: f64::from(p.dimensions.width),
|
||||
height: f64::from(p.dimensions.height),
|
||||
protector: None,
|
||||
};
|
||||
let slot = if self.held().is_some_and(|s| &*s.item == TOTEM) {
|
||||
Some(self.selected)
|
||||
// `checkTotemDeathProtection`: the selected slot first, then the
|
||||
// offhand. The copy of that stack is the protector (`protectionItem`)
|
||||
// the use is counted and the activation animation shown with, taken
|
||||
// before the use takes one off the stack.
|
||||
let main = self.held().is_some_and(|s| self.has_death_protection(s));
|
||||
let cell = if main {
|
||||
&mut self.inventory[self.selected]
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cell = match slot {
|
||||
Some(i) => &mut self.inventory[i],
|
||||
None => &mut self.offhand,
|
||||
&mut self.offhand
|
||||
};
|
||||
let protector = cell
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ItemStack::new(TOTEM, 1));
|
||||
if let Some(s) = cell.as_mut() {
|
||||
s.count -= 1;
|
||||
if s.count == 0 {
|
||||
*cell = None;
|
||||
}
|
||||
}
|
||||
self.award_stat("used", TOTEM, 1);
|
||||
self.trigger_item(
|
||||
"minecraft:used_totem",
|
||||
&horizon_client::items::ItemStack::new(TOTEM, 1),
|
||||
);
|
||||
self.award_stat("used", &protector.item, 1);
|
||||
self.trigger_item("minecraft:used_totem", &protector);
|
||||
// ItemStack.causeUseVibration: the totem's use effects make one.
|
||||
let ctx = self.player_context(None);
|
||||
self.post_game_event("minecraft:item_interact_finish", body.at, ctx);
|
||||
// DeathProtection.TOTEM_EFFECTS: clear, then regeneration II, absorption II, fire resistance.
|
||||
self.potions.effects.clear();
|
||||
for (id, ticks, amp) in [
|
||||
("regeneration", 900, 1),
|
||||
("absorption", 100, 1),
|
||||
("fire_resistance", 800, 0),
|
||||
] {
|
||||
if let Some(e) = horizon_client::brewing::effect(id) {
|
||||
self.add_player_effect(EffectInstance::new(e.id, ticks, amp));
|
||||
}
|
||||
// `DeathProtection.applyEffects`: the component's own `death_effects`,
|
||||
// read from the protector copy (the totem's clear-then-effects, or a
|
||||
// custom list; `death_protection={}` has none).
|
||||
for effect in self.death_effects(&protector) {
|
||||
self.apply_consume_effect(&effect);
|
||||
}
|
||||
Some(body)
|
||||
Some(Body {
|
||||
protector: Some(protector),
|
||||
..body
|
||||
})
|
||||
}
|
||||
/// The window's player's totem: the server's half, then the event to
|
||||
/// the guests and to this window's own client.
|
||||
@@ -97,12 +144,14 @@ impl Game<'_> {
|
||||
at: [p.position.x, p.position.y, p.position.z],
|
||||
width: f64::from(p.dimensions.width),
|
||||
height: f64::from(p.dimensions.height),
|
||||
protector: None,
|
||||
})
|
||||
} else {
|
||||
self.mobs.iter().find(|m| m.id == id as u32).map(|m| Body {
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
width: f64::from(m.width()),
|
||||
height: f64::from(m.height()),
|
||||
protector: None,
|
||||
})
|
||||
};
|
||||
if let Some(body) = body {
|
||||
@@ -126,8 +175,10 @@ impl Game<'_> {
|
||||
self.pad_rumble(horizon_client::gamepad::Rumble::Totem);
|
||||
// findTotem, then LocalPlayer.displayItemActivation. The offsets
|
||||
// come from the player's own random in vanilla, not the level's:
|
||||
// the client-only particle random stands in for it.
|
||||
let totem = self.find_totem();
|
||||
// the client-only particle random stands in for it. The server
|
||||
// half hands over the stack it used up; only an event from a
|
||||
// remote server, whose item is still in the hand, looks one up.
|
||||
let totem = body.protector.unwrap_or_else(|| self.find_totem());
|
||||
self.item_activation
|
||||
.activate(totem, self.particles.random());
|
||||
}
|
||||
@@ -136,16 +187,10 @@ impl Game<'_> {
|
||||
/// `death_protection`, else a new totem (a used single totem is gone by
|
||||
/// the time the event arrives).
|
||||
fn find_totem(&self) -> ItemStack {
|
||||
let models = &self.pack.item_renderer.models;
|
||||
[self.held(), self.offhand.as_ref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|s| {
|
||||
models
|
||||
.components(&horizon_client::item_render::stack_view(s))
|
||||
.get("minecraft:death_protection")
|
||||
.is_some()
|
||||
})
|
||||
.find(|s| self.has_death_protection(s))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ItemStack::new(TOTEM, 1))
|
||||
}
|
||||
@@ -190,10 +235,26 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where an entity event's entity stands and its size.
|
||||
#[derive(Clone, Copy)]
|
||||
/// The `death_effects` of the stack's `minecraft:death_protection`, if it has
|
||||
/// the component.
|
||||
fn death_effects_of(
|
||||
models: &horizon_assets::item_model::ItemModels,
|
||||
s: &ItemStack,
|
||||
) -> Vec<horizon_client::items::ConsumeEffect> {
|
||||
models
|
||||
.components(&horizon_client::item_render::stack_view(s))
|
||||
.get("minecraft:death_protection")
|
||||
.map(horizon_client::items::death_effects)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Where an entity event's entity stands and its size, and, for the event
|
||||
/// this window's own player fires after the server half used a stack up, the
|
||||
/// protector copy the activation animation shows.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Body {
|
||||
pub at: [f64; 3],
|
||||
width: f64,
|
||||
height: f64,
|
||||
protector: Option<ItemStack>,
|
||||
}
|
||||
@@ -1328,6 +1328,17 @@ pub struct Extra {
|
||||
/// (`LivingEntity.lastEquipmentItems`, empty at first), by
|
||||
/// `EquipmentSlot` ordinal: see `Mob::equipment_stacks`.
|
||||
pub equipment_heard: Option<Box<[Option<crate::items::ItemStack>; 8]>>,
|
||||
/// What the game last read from the hands' `minecraft:death_protection`
|
||||
/// components (`game/totem.rs`); `None` until it has, when the totem's id
|
||||
/// stands in for the component.
|
||||
pub hand_protection: Option<HandProtection>,
|
||||
}
|
||||
/// The `death_effects` of the `death_protection` component each hand's stack
|
||||
/// carries; `None` for a hand without the component.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct HandProtection {
|
||||
pub main: Option<Vec<crate::items::ConsumeEffect>>,
|
||||
pub off: Option<Vec<crate::items::ConsumeEffect>>,
|
||||
}
|
||||
impl Extra {
|
||||
pub fn set_main_hand(&mut self, item: Option<Arc<str>>) {
|
||||
@@ -1349,6 +1360,30 @@ impl Extra {
|
||||
}
|
||||
}
|
||||
|
||||
/// `DeathProtection.TOTEM_EFFECTS`: clear, then regeneration II, absorption II
|
||||
/// and fire resistance.
|
||||
fn totem_death_effects() -> Vec<crate::items::ConsumeEffect> {
|
||||
use crate::items::{ConsumeEffect, ConsumeEffectSpec};
|
||||
let spec = |id: &str, duration, amplifier| ConsumeEffectSpec {
|
||||
id: id.to_owned(),
|
||||
duration,
|
||||
amplifier,
|
||||
ambient: false,
|
||||
show_particles: true,
|
||||
};
|
||||
vec![
|
||||
ConsumeEffect::ClearAllEffects,
|
||||
ConsumeEffect::ApplyEffects {
|
||||
effects: vec![
|
||||
spec("minecraft:regeneration", 900, 1),
|
||||
spec("minecraft:absorption", 100, 1),
|
||||
spec("minecraft:fire_resistance", 800, 0),
|
||||
],
|
||||
probability: 1.0,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// How far the boxes of a set of mobs reach from their positions: the widest half width and the
|
||||
/// tallest height among them. A box query over many mobs (a vehicle's, against every mob of the
|
||||
/// level) tests each mob's position against this first, and works out the box ([`Mob::bounds`],
|
||||
@@ -4003,34 +4038,52 @@ impl Mob {
|
||||
through
|
||||
}
|
||||
/// `LivingEntity.checkTotemDeathProtection`: the first hand with a
|
||||
/// `death_protection` item uses it up, the mob survives at 1 health with
|
||||
/// the totem's effects, and entity event 35 tells the clients.
|
||||
/// `death_protection` stack uses one item up, the mob survives at 1 health
|
||||
/// with that component's `death_effects`, and entity event 35 tells the
|
||||
/// clients.
|
||||
fn use_death_protection(&mut self, source: &Damage) -> bool {
|
||||
// `DamageTypeTags.BYPASSES_INVULNERABILITY`: the void and /kill take it.
|
||||
if source.kind.bypasses_invulnerability() {
|
||||
return false;
|
||||
}
|
||||
// `getItemInHand`: the main hand first. The totem is the only item in
|
||||
// the shipped data with `minecraft:death_protection`.
|
||||
// `getItemInHand`: the main hand first. The game reads the components
|
||||
// (`hand_protection`); before it has, the totem is the only item in the
|
||||
// shipped data with `minecraft:death_protection`.
|
||||
const TOTEM: &str = "minecraft:totem_of_undying";
|
||||
let main = self.extra.main_hand.as_deref() == Some(TOTEM);
|
||||
let off = !main && self.extra.off_hand.as_deref() == Some(TOTEM);
|
||||
if !main && !off {
|
||||
return false;
|
||||
}
|
||||
if main {
|
||||
self.extra.set_main_hand(None);
|
||||
} else {
|
||||
let (main, off) = match self.extra.hand_protection.clone() {
|
||||
Some(h) => (h.main, h.off),
|
||||
None => {
|
||||
let totem = || Some(totem_death_effects());
|
||||
(
|
||||
(self.extra.main_hand.as_deref() == Some(TOTEM))
|
||||
.then(totem)
|
||||
.flatten(),
|
||||
(self.extra.off_hand.as_deref() == Some(TOTEM))
|
||||
.then(totem)
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
};
|
||||
// `itemStack.shrink(1)`: a held stack of several keeps the rest.
|
||||
let effects = if let Some(effects) = main {
|
||||
match self.extra.main_hand_stack.as_mut() {
|
||||
Some(s) if s.count > 1 && self.extra.main_hand.as_ref() == Some(&s.item) => {
|
||||
s.count -= 1;
|
||||
}
|
||||
_ => self.extra.set_main_hand(None),
|
||||
}
|
||||
effects
|
||||
} else if let Some(effects) = off {
|
||||
self.extra.off_hand = None;
|
||||
}
|
||||
// `setHealth(1.0F)`, then `DeathProtection.TOTEM_EFFECTS`.
|
||||
effects
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
self.extra.hand_protection = None;
|
||||
// `setHealth(1.0F)`, then `DeathProtection.applyEffects`.
|
||||
self.health = 1.0;
|
||||
for (effect, ticks, amplifier) in [
|
||||
("regeneration", 900, 1),
|
||||
("absorption", 100, 1),
|
||||
("fire_resistance", 800, 0),
|
||||
] {
|
||||
self.add_effect(effect, ticks, amplifier);
|
||||
for effect in &effects {
|
||||
self.apply_death_effect(effect);
|
||||
}
|
||||
let at = [self.position.x, self.position.y, self.position.z];
|
||||
self.outbox.push(MobEvent::EntityEvent {
|
||||
@@ -4040,6 +4093,57 @@ impl Mob {
|
||||
});
|
||||
true
|
||||
}
|
||||
/// One `ConsumeEffect` of a `death_protection` component on the mob.
|
||||
/// Teleporting, sounds and a mod's own kinds are not applied to mobs.
|
||||
fn apply_death_effect(&mut self, effect: &crate::items::ConsumeEffect) {
|
||||
use crate::items::ConsumeEffect;
|
||||
match effect {
|
||||
// `LivingEntity.removeAllEffects`: the absorption hearts go with it.
|
||||
ConsumeEffect::ClearAllEffects => {
|
||||
for e in std::mem::take(&mut self.extra.effects) {
|
||||
if e.0 == "minecraft:absorption" {
|
||||
self.absorption = (self.absorption - 4.0 * (e.2 + 1) as f32).max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConsumeEffect::RemoveEffects(ids) => {
|
||||
self.extra.effects.retain(|e| {
|
||||
let held = e.0.trim_start_matches("minecraft:");
|
||||
!ids.iter()
|
||||
.any(|id| id.trim_start_matches("minecraft:") == held)
|
||||
});
|
||||
}
|
||||
// `ApplyStatusEffectsConsumeEffect.apply`: one random draw, even at
|
||||
// probability 1.
|
||||
ConsumeEffect::ApplyEffects {
|
||||
effects,
|
||||
probability,
|
||||
} => {
|
||||
if self.random.next_float() >= *probability {
|
||||
return;
|
||||
}
|
||||
for spec in effects {
|
||||
let Some(def) = horizon_client_core::potions::effect(&spec.id) else {
|
||||
continue;
|
||||
};
|
||||
if def.instant {
|
||||
match def.id {
|
||||
"instant_damage" | "instant_health" => self.apply_heal_or_harm(
|
||||
def.id == "instant_damage",
|
||||
self.kind.flags.undead,
|
||||
spec.amplifier,
|
||||
1.0,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
self.add_effect(def.id, spec.duration, spec.amplifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// `LivingEntity.heal`, with the native heal hook.
|
||||
pub fn heal(&mut self, mut amount: f32) {
|
||||
let mut cancel = false;
|
||||
|
||||
@@ -2306,6 +2306,62 @@ pub fn bucket_contents(
|
||||
}
|
||||
|
||||
/// One `MobEffectInstance` of an `apply_effects` consume effect (its codec's defaults).
|
||||
/// `DeathProtection.CODEC`: the `death_effects` of a `minecraft:death_protection`
|
||||
/// component, in order (`applyEffects` runs each one). A missing or empty
|
||||
/// list, as in `death_protection={}`, has none.
|
||||
pub fn death_effects(component: &Value) -> Vec<ConsumeEffect> {
|
||||
component
|
||||
.get("death_effects")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(parse_consume_effect)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod death_effects_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn the_totems_component_reads_clear_then_three_effects() {
|
||||
let totem = serde_json::json!({"death_effects": [
|
||||
{"type": "minecraft:clear_all_effects"},
|
||||
{"type": "minecraft:apply_effects", "effects": [
|
||||
{"amplifier": 1, "duration": 900, "id": "minecraft:regeneration", "show_icon": true},
|
||||
{"amplifier": 1, "duration": 100, "id": "minecraft:absorption", "show_icon": true},
|
||||
{"duration": 800, "id": "minecraft:fire_resistance", "show_icon": true}
|
||||
]}
|
||||
]});
|
||||
let effects = death_effects(&totem);
|
||||
assert_eq!(effects.len(), 2);
|
||||
assert_eq!(effects[0], ConsumeEffect::ClearAllEffects);
|
||||
let ConsumeEffect::ApplyEffects {
|
||||
effects,
|
||||
probability,
|
||||
} = &effects[1]
|
||||
else {
|
||||
panic!("second effect is apply_effects");
|
||||
};
|
||||
assert_eq!(*probability, 1.0);
|
||||
let ids: Vec<_> = effects
|
||||
.iter()
|
||||
.map(|e| (e.id.as_str(), e.duration, e.amplifier))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
ids,
|
||||
[
|
||||
("minecraft:regeneration", 900, 1),
|
||||
("minecraft:absorption", 100, 1),
|
||||
("minecraft:fire_resistance", 800, 0)
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn an_empty_component_has_no_effects() {
|
||||
assert!(death_effects(&serde_json::json!({})).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_effect_spec(e: &Value) -> Option<ConsumeEffectSpec> {
|
||||
let flag = |key: &str, default: bool| e.get(key).and_then(|v| v.as_bool()).unwrap_or(default);
|
||||
Some(ConsumeEffectSpec {
|
||||
|
||||
Reference in new issue
Block a user