proof_engine/metaball/
damage.rs1use glam::Vec3;
5use super::entity_field::MetaballEntity;
6
7#[derive(Debug, Clone)]
9pub struct DamageEvent {
10 pub impact_point: Vec3,
12 pub damage: f32,
14 pub max_hp: f32,
16 pub is_crit: bool,
18 pub knockback_dir: Option<Vec3>,
20 pub time: f32,
22}
23
24#[derive(Debug, Clone)]
26pub struct DamageResponse {
27 pub hit_source: Option<usize>,
29 pub source_destroyed: bool,
31 pub deformation: f32,
33 pub entity_died: bool,
35 pub particle_burst: Option<(Vec3, u32)>,
37}
38
39pub struct DamageSystem;
41
42impl DamageSystem {
43 pub fn apply_damage(entity: &mut MetaballEntity, event: &DamageEvent) -> DamageResponse {
45 let damage_ratio = event.damage / event.max_hp.max(1.0);
46
47 let hit_source = entity.nearest_source(event.impact_point);
49
50 let mut source_destroyed = false;
51 let mut deformation = 0.0;
52
53 if let Some(idx) = hit_source {
54 let source = &mut entity.sources[idx];
55
56 if event.is_crit {
57 source.destroyed = true;
59 source.strength = 0.0;
60 source_destroyed = true;
61 deformation = source.base_strength;
62 } else {
63 let reduction = damage_ratio * source.base_strength;
66 source.damage_reduction += reduction;
67
68 let max_reduction = source.base_strength * 0.9;
70 source.damage_reduction = source.damage_reduction.min(max_reduction);
71
72 source.last_hit_time = event.time;
73 deformation = reduction;
74 }
75
76 if let Some(kb_dir) = event.knockback_dir {
78 let kb_strength = damage_ratio * 0.3;
79 source.position += kb_dir * kb_strength;
80 }
81 }
82
83 entity.set_hp(entity.hp_ratio - damage_ratio);
85
86 let entity_died = entity.is_dead();
88
89 let particle_burst = if source_destroyed {
91 Some((event.impact_point, 50))
92 } else if deformation > 0.1 {
93 Some((event.impact_point, (deformation * 20.0) as u32))
94 } else {
95 None
96 };
97
98 entity.dirty = true;
99
100 DamageResponse {
101 hit_source,
102 source_destroyed,
103 deformation,
104 entity_died,
105 particle_burst,
106 }
107 }
108
109 pub fn death_sequence(entity: &mut MetaballEntity, progress: f32) {
113 let p = progress.clamp(0.0, 1.0);
114
115 for source in &mut entity.sources {
116 if source.destroyed { continue; }
117
118 let contract_factor = 1.0 - p;
120 source.position = entity.center + source.rest_offset * contract_factor * entity.scale;
121
122 let decay = (1.0 - p).powi(2);
124 source.strength = source.base_strength * decay;
125
126 let original_radius = source.radius;
128 source.radius = original_radius * (1.0 - p * 0.8); if p > 0.95 {
132 source.destroyed = true;
133 source.strength = 0.0;
134 }
135 }
136
137 entity.hp_ratio = 0.0;
138 entity.dirty = true;
139 }
140
141 pub fn low_hp_effects(entity: &mut MetaballEntity) {
145 if entity.hp_ratio > 0.3 { return; }
146
147 let fragility = 1.0 - (entity.hp_ratio / 0.3);
148
149 for source in &mut entity.sources {
150 if source.destroyed { continue; }
151
152 source.emission = source.emission.max(fragility * 0.8);
154
155 source.breath_amplitude = 0.02 + fragility * 0.05;
157 }
158 }
159
160 pub fn apply_aoe_damage(
162 entity: &mut MetaballEntity,
163 center: Vec3,
164 radius: f32,
165 damage: f32,
166 max_hp: f32,
167 time: f32,
168 ) -> Vec<DamageResponse> {
169 let mut responses = Vec::new();
170 let damage_ratio = damage / max_hp.max(1.0);
171
172 for (idx, source) in entity.sources.iter_mut().enumerate() {
173 if source.destroyed { continue; }
174 let dist = (source.position - center).length();
175 if dist > radius { continue; }
176
177 let falloff = 1.0 - (dist / radius);
178 let local_damage = damage_ratio * falloff;
179 let reduction = local_damage * source.base_strength;
180 source.damage_reduction += reduction;
181 source.last_hit_time = time;
182
183 responses.push(DamageResponse {
184 hit_source: Some(idx),
185 source_destroyed: false,
186 deformation: reduction,
187 entity_died: false,
188 particle_burst: if reduction > 0.05 {
189 Some((source.position, (reduction * 10.0) as u32))
190 } else { None },
191 });
192 }
193
194 entity.set_hp(entity.hp_ratio - damage_ratio);
195 entity.dirty = true;
196 responses
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use super::super::entity_field::FieldSource;
204
205 fn test_entity() -> MetaballEntity {
206 let mut e = MetaballEntity::new(0.5, 16);
207 e.add_source(FieldSource::new(Vec3::ZERO, 1.0, 1.0).with_tag("center"));
208 e.add_source(FieldSource::new(Vec3::X, 0.8, 0.8).with_tag("right"));
209 e.add_source(FieldSource::new(Vec3::NEG_X, 0.8, 0.8).with_tag("left"));
210 e
211 }
212
213 #[test]
214 fn normal_hit_reduces_strength() {
215 let mut e = test_entity();
216 let before = e.sources[0].base_strength;
217 let event = DamageEvent {
218 impact_point: Vec3::ZERO, damage: 10.0, max_hp: 100.0,
219 is_crit: false, knockback_dir: None, time: 1.0,
220 };
221 let resp = DamageSystem::apply_damage(&mut e, &event);
222 assert!(resp.hit_source == Some(0));
223 assert!(!resp.source_destroyed);
224 assert!(e.sources[0].damage_reduction > 0.0);
225 }
226
227 #[test]
228 fn crit_destroys_source() {
229 let mut e = test_entity();
230 let event = DamageEvent {
231 impact_point: Vec3::X * 0.9, damage: 20.0, max_hp: 100.0,
232 is_crit: true, knockback_dir: None, time: 1.0,
233 };
234 let resp = DamageSystem::apply_damage(&mut e, &event);
235 assert!(resp.source_destroyed);
236 assert_eq!(resp.hit_source, Some(1));
238 assert!(e.sources[1].destroyed);
239 }
240
241 #[test]
242 fn death_sequence_removes_all() {
243 let mut e = test_entity();
244 e.set_hp(0.0);
245 DamageSystem::death_sequence(&mut e, 1.0);
246 assert!(e.active_source_count() == 0);
247 }
248
249 #[test]
250 fn low_hp_increases_emission() {
251 let mut e = test_entity();
252 e.set_hp(0.1);
253 let emission_before = e.sources[0].emission;
254 DamageSystem::low_hp_effects(&mut e);
255 assert!(e.sources[0].emission > emission_before);
256 }
257
258 #[test]
259 fn aoe_damage_affects_multiple() {
260 let mut e = test_entity();
261 let responses = DamageSystem::apply_aoe_damage(
262 &mut e, Vec3::ZERO, 2.0, 10.0, 100.0, 1.0,
263 );
264 assert!(responses.len() >= 2, "AoE should hit multiple sources");
265 }
266
267 #[test]
268 fn lethal_damage_kills() {
269 let mut e = test_entity();
270 let event = DamageEvent {
271 impact_point: Vec3::ZERO, damage: 100.0, max_hp: 100.0,
272 is_crit: false, knockback_dir: None, time: 1.0,
273 };
274 let resp = DamageSystem::apply_damage(&mut e, &event);
275 assert!(resp.entity_died);
276 }
277
278 #[test]
279 fn death_sequence_progressive() {
280 let mut e = test_entity();
281 e.set_hp(0.0);
282 DamageSystem::death_sequence(&mut e, 0.0);
283 assert!(e.active_source_count() > 0, "At progress=0, sources should still exist");
284 DamageSystem::death_sequence(&mut e, 0.5);
285 let mid_strength: f32 = e.sources.iter().map(|s| s.strength).sum();
286 assert!(mid_strength > 0.0 && mid_strength < 3.0);
287 }
288}