Skip to main content

steel_core/entity/damage/
source.rs

1use std::fmt;
2use std::sync::Arc;
3
4use glam::DVec3;
5use steel_registry::{
6    REGISTRY, TaggedRegistryExt, damage_type::DamageScaling, damage_type::DamageType,
7    vanilla_damage_type_tags,
8};
9
10use crate::entity::SharedEntity;
11use crate::player::Player;
12
13/// Describes damage while retaining the exact direct and causing entity allocations.
14///
15/// Removal and respawn never rebind attribution. Store recent sources in
16/// [`super::DamageHistory`] to avoid self/mutual ownership cycles.
17#[derive(Clone)]
18pub struct DamageSource {
19    /// The damage type registry entry.
20    pub damage_type: &'static DamageType,
21    causing_entity: Option<SharedEntity>,
22    direct_entity: Option<SharedEntity>,
23    source_position: Option<DVec3>,
24}
25
26impl DamageSource {
27    /// Environmental damage with no entity or position context (void, starvation, etc.).
28    #[must_use]
29    pub const fn environment(damage_type: &'static DamageType) -> Self {
30        Self {
31            damage_type,
32            causing_entity: None,
33            direct_entity: None,
34            source_position: None,
35        }
36    }
37
38    /// Damage delivered directly by its causing entity.
39    #[must_use]
40    pub fn direct(damage_type: &'static DamageType, entity: SharedEntity) -> Self {
41        Self {
42            damage_type,
43            causing_entity: Some(Arc::clone(&entity)),
44            direct_entity: Some(entity),
45            source_position: None,
46        }
47    }
48
49    /// Adds the entity ultimately responsible for the damage.
50    #[must_use]
51    pub fn with_causing_entity(mut self, entity: SharedEntity) -> Self {
52        self.causing_entity = Some(entity);
53        self
54    }
55
56    /// Adds the direct entity that delivered the damage.
57    #[must_use]
58    pub fn with_direct_entity(mut self, entity: SharedEntity) -> Self {
59        self.direct_entity = Some(entity);
60        self
61    }
62
63    /// The original cause (e.g. a projectile's shooter), retained after removal.
64    #[must_use]
65    pub const fn causing_entity(&self) -> Option<&SharedEntity> {
66        self.causing_entity.as_ref()
67    }
68
69    /// The original direct entity (e.g. the projectile), retained after removal.
70    #[must_use]
71    pub const fn direct_entity(&self) -> Option<&SharedEntity> {
72        self.direct_entity.as_ref()
73    }
74
75    /// The explicit source position, or the direct entity's position when none was set.
76    /// Distinct from the raw position sent in packets.
77    #[must_use]
78    pub fn source_position(&self) -> Option<DVec3> {
79        self.source_position
80            .or_else(|| self.direct_entity().map(|entity| entity.position()))
81    }
82
83    /// The explicit source position without the direct-entity fallback, used by the
84    /// damage-event packet.
85    #[must_use]
86    pub const fn source_position_raw(&self) -> Option<DVec3> {
87        self.source_position
88    }
89
90    /// Whether the causing player has vanilla infinite-materials abilities.
91    #[must_use]
92    pub fn is_creative_player(&self) -> bool {
93        self.causing_entity()
94            .and_then(|entity| entity.as_player())
95            .is_some_and(Player::has_infinite_materials)
96    }
97
98    /// Adds the vanilla source position used by damage events and knockback.
99    #[must_use]
100    pub const fn with_source_position(mut self, source_position: DVec3) -> Self {
101        self.source_position = Some(source_position);
102        self
103    }
104
105    /// Whether this damage bypasses creative/spectator invulnerability.
106    #[must_use]
107    pub fn bypasses_invulnerability(&self) -> bool {
108        self.is(&vanilla_damage_type_tags::DamageTypeTag::BYPASSES_INVULNERABILITY)
109    }
110
111    /// Returns whether this damage type is in the given vanilla damage-type tag.
112    #[must_use]
113    pub fn is(&self, tag: &steel_utils::Identifier) -> bool {
114        REGISTRY.damage_types.is_in_tag(self.damage_type, tag)
115    }
116
117    /// Whether the damage was dealt directly by the causing entity rather than
118    /// indirectly, e.g. by a thrown projectile.
119    #[must_use]
120    pub fn is_direct(&self) -> bool {
121        match (self.causing_entity(), self.direct_entity()) {
122            (None, None) => true,
123            (Some(cause), Some(direct)) => Arc::ptr_eq(cause, direct),
124            _ => false,
125        }
126    }
127
128    /// Whether this damage bypasses the invulnerability cooldown timer.
129    /// No vanilla damage types currently use this, but the logic exists in
130    /// `LivingEntity.hurtServer()`.
131    /// TODO: use damage type tag query once supported
132    #[expect(clippy::unused_self, reason = "this is an api function")]
133    #[must_use]
134    pub const fn bypasses_cooldown(&self) -> bool {
135        false
136    }
137
138    /// Whether this damage scales with difficulty for the retained causing entity.
139    #[must_use]
140    pub fn scales_with_difficulty(&self) -> bool {
141        let causing_entity = self.causing_entity();
142        match self.damage_type.scaling {
143            DamageScaling::Never => false,
144            DamageScaling::WhenCausedByLivingNonPlayer => causing_entity.is_some_and(|entity| {
145                entity.as_living_entity().is_some() && entity.as_player().is_none()
146            }),
147            DamageScaling::Always => true,
148        }
149    }
150}
151
152impl fmt::Debug for DamageSource {
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        formatter
155            .debug_struct("DamageSource")
156            .field("damage_type", &self.damage_type)
157            .field(
158                "causing_entity",
159                &self
160                    .causing_entity
161                    .as_ref()
162                    .map(|entity| entity.generation()),
163            )
164            .field(
165                "direct_entity",
166                &self
167                    .direct_entity
168                    .as_ref()
169                    .map(|entity| entity.generation()),
170            )
171            .field("source_position", &self.source_position)
172            .finish()
173    }
174}