Skip to main content

steel_core/entity/
damage.rs

1//! Damage source system.
2
3use glam::DVec3;
4use steel_registry::{
5    REGISTRY, TaggedRegistryExt, damage_type::DamageScaling, damage_type::DamageType,
6    vanilla_damage_type_tags,
7};
8
9use crate::entity::Entity;
10
11/// Describes how an entity was damaged.
12#[derive(Debug, Clone)]
13pub struct DamageSource {
14    /// The damage type registry entry.
15    pub damage_type: &'static DamageType,
16    /// The entity ultimately responsible (e.g. the shooter for projectiles).
17    pub causing_entity_id: Option<i32>,
18    /// The entity that directly dealt the damage (e.g. the projectile itself).
19    pub direct_entity_id: Option<i32>,
20    /// Source position (for explosions, etc.).
21    pub source_position: Option<DVec3>,
22}
23
24impl DamageSource {
25    /// Environmental damage with no entity or position context (void, starvation, etc.).
26    #[must_use]
27    pub const fn environment(damage_type: &'static DamageType) -> Self {
28        Self {
29            damage_type,
30            causing_entity_id: None,
31            direct_entity_id: None,
32            source_position: None,
33        }
34    }
35
36    /// Adds the entity ultimately responsible for the damage.
37    #[must_use]
38    pub const fn with_causing_entity(mut self, entity_id: i32) -> Self {
39        self.causing_entity_id = Some(entity_id);
40        self
41    }
42
43    /// Adds the direct entity that delivered the damage.
44    #[must_use]
45    pub const fn with_direct_entity(mut self, entity_id: i32) -> Self {
46        self.direct_entity_id = Some(entity_id);
47        self
48    }
49
50    /// Adds the vanilla source position used by damage events and knockback.
51    #[must_use]
52    pub const fn with_source_position(mut self, source_position: DVec3) -> Self {
53        self.source_position = Some(source_position);
54        self
55    }
56
57    /// Whether this damage bypasses creative/spectator invulnerability.
58    #[must_use]
59    pub fn bypasses_invulnerability(&self) -> bool {
60        self.is(&vanilla_damage_type_tags::DamageTypeTag::BYPASSES_INVULNERABILITY)
61    }
62
63    /// Returns whether this damage type is in the given vanilla damage-type tag.
64    #[must_use]
65    pub fn is(&self, tag: &steel_utils::Identifier) -> bool {
66        REGISTRY.damage_types.is_in_tag(self.damage_type, tag)
67    }
68
69    /// Returns vanilla `DamageSource.isDirect`.
70    #[must_use]
71    pub fn is_direct(&self) -> bool {
72        self.causing_entity_id == self.direct_entity_id
73    }
74
75    /// Whether this damage bypasses the invulnerability cooldown timer.
76    /// No vanilla damage types currently use this, but the logic exists in
77    /// `LivingEntity.hurtServer()`.
78    /// TODO: use damage type tag query once supported
79    #[expect(clippy::unused_self, reason = "this is an api function")]
80    #[must_use]
81    pub const fn bypasses_cooldown(&self) -> bool {
82        false
83    }
84
85    /// Whether this damage scales with world difficulty for the resolved causing entity.
86    ///
87    /// `causing_entity` is `None` when the source has no cause or its stored entity ID no
88    /// longer resolves. Both cases fail Vanilla's living non-player type check.
89    #[must_use]
90    pub fn scales_with_difficulty(&self, causing_entity: Option<&dyn Entity>) -> bool {
91        match self.damage_type.scaling {
92            DamageScaling::Never => false,
93            DamageScaling::WhenCausedByLivingNonPlayer => causing_entity.is_some_and(|entity| {
94                entity.as_living_entity().is_some() && entity.as_player().is_none()
95            }),
96            DamageScaling::Always => true,
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use std::sync::Weak;
104
105    use glam::DVec3;
106    use steel_registry::{init_vanilla_registry, vanilla_damage_types, vanilla_entities};
107
108    use crate::entity::entities::{FireworkRocketEntity, PigEntity};
109
110    use super::*;
111
112    #[test]
113    fn conditional_difficulty_scaling_requires_a_resolved_living_non_player() {
114        init_vanilla_registry();
115        let source = DamageSource::environment(&vanilla_damage_types::FIREWORKS);
116        let pig = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
117        let rocket = FireworkRocketEntity::new(
118            &vanilla_entities::FIREWORK_ROCKET,
119            2,
120            DVec3::ZERO,
121            Weak::new(),
122        );
123
124        assert!(source.scales_with_difficulty(Some(&pig)));
125        assert!(!source.scales_with_difficulty(Some(&rocket)));
126        assert!(!source.scales_with_difficulty(None));
127    }
128}