1use glam::{Vec3, Vec4};
5use super::entity_field::{MetaballEntity, FieldSource};
6
7#[derive(Debug, Clone)]
9pub struct FieldSample {
10 pub strength: f32,
12 pub color: Vec4,
14 pub emission: f32,
16 pub gradient: Vec3,
18 pub above_threshold: bool,
20 pub signed_distance_estimate: f32,
22}
23
24impl Default for FieldSample {
25 fn default() -> Self {
26 Self {
27 strength: 0.0,
28 color: Vec4::new(0.5, 0.5, 0.5, 1.0),
29 emission: 0.0,
30 gradient: Vec3::ZERO,
31 above_threshold: false,
32 signed_distance_estimate: 0.0,
33 }
34 }
35}
36
37pub struct FieldEvaluator {
39 pub gradient_eps: f32,
41 pub compute_gradient: bool,
43 pub compute_material: bool,
45 pub min_contribution: f32,
47}
48
49impl Default for FieldEvaluator {
50 fn default() -> Self {
51 Self {
52 gradient_eps: 0.01,
53 compute_gradient: true,
54 compute_material: true,
55 min_contribution: 1e-6,
56 }
57 }
58}
59
60impl FieldEvaluator {
61 pub fn strength_only() -> Self {
62 Self { compute_gradient: false, compute_material: false, ..Default::default() }
63 }
64
65 pub fn with_gradient() -> Self {
66 Self { compute_gradient: true, compute_material: false, ..Default::default() }
67 }
68
69 pub fn full() -> Self { Self::default() }
70
71 pub fn evaluate(&self, entity: &MetaballEntity, point: Vec3) -> FieldSample {
73 let mut total_strength = 0.0f32;
74 let mut weighted_color = Vec4::ZERO;
75 let mut weighted_emission = 0.0f32;
76
77 for source in &entity.sources {
79 if !source.is_active() { continue; }
80
81 let to_source = source.position - point;
83 let dist_sq = to_source.length_squared();
84 let max_dist = source.radius * 1.1; if dist_sq > max_dist * max_dist { continue; }
86
87 let contrib = source.evaluate(point, entity.hp_ratio);
88 if contrib < self.min_contribution { continue; }
89
90 total_strength += contrib;
91
92 if self.compute_material {
93 weighted_color += source.color * contrib;
94 weighted_emission += source.emission * contrib;
95 }
96 }
97
98 let color = if total_strength > 1e-8 && self.compute_material {
100 weighted_color / total_strength
101 } else {
102 Vec4::new(0.5, 0.5, 0.5, 1.0)
103 };
104
105 let emission = if total_strength > 1e-8 && self.compute_material {
106 weighted_emission / total_strength
107 } else {
108 0.0
109 };
110
111 let gradient = if self.compute_gradient {
113 let eps = self.gradient_eps;
114 Vec3::new(
115 evaluate_strength(entity, point + Vec3::X * eps)
116 - evaluate_strength(entity, point - Vec3::X * eps),
117 evaluate_strength(entity, point + Vec3::Y * eps)
118 - evaluate_strength(entity, point - Vec3::Y * eps),
119 evaluate_strength(entity, point + Vec3::Z * eps)
120 - evaluate_strength(entity, point - Vec3::Z * eps),
121 ) / (2.0 * eps)
122 } else {
123 Vec3::ZERO
124 };
125
126 let above = total_strength >= entity.threshold;
127 let sde = (total_strength - entity.threshold) / gradient.length().max(1e-6);
128
129 FieldSample {
130 strength: total_strength,
131 color,
132 emission,
133 gradient,
134 above_threshold: above,
135 signed_distance_estimate: sde,
136 }
137 }
138
139 pub fn evaluate_strength_at(&self, entity: &MetaballEntity, point: Vec3) -> f32 {
141 evaluate_strength(entity, point)
142 }
143
144 pub fn sample_grid(
146 &self,
147 entity: &MetaballEntity,
148 bounds_min: Vec3,
149 bounds_max: Vec3,
150 resolution: u32,
151 ) -> FieldGrid {
152 let res = resolution as usize;
153 let total = res * res * res;
154 let step = (bounds_max - bounds_min) / (resolution - 1) as f32;
155
156 let mut strengths = Vec::with_capacity(total);
157 let mut colors = if self.compute_material { Vec::with_capacity(total) } else { Vec::new() };
158 let mut emissions = if self.compute_material { Vec::with_capacity(total) } else { Vec::new() };
159
160 for z in 0..res {
161 for y in 0..res {
162 for x in 0..res {
163 let point = bounds_min + Vec3::new(x as f32, y as f32, z as f32) * step;
164
165 if self.compute_material {
166 let (s, c, e) = entity.evaluate_full(point);
167 strengths.push(s);
168 colors.push(c);
169 emissions.push(e);
170 } else {
171 strengths.push(evaluate_strength(entity, point));
172 }
173 }
174 }
175 }
176
177 FieldGrid {
178 strengths,
179 colors,
180 emissions,
181 resolution,
182 bounds_min,
183 bounds_max,
184 step,
185 }
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct FieldGrid {
192 pub strengths: Vec<f32>,
193 pub colors: Vec<Vec4>,
194 pub emissions: Vec<f32>,
195 pub resolution: u32,
196 pub bounds_min: Vec3,
197 pub bounds_max: Vec3,
198 pub step: Vec3,
199}
200
201impl FieldGrid {
202 pub fn index(&self, x: usize, y: usize, z: usize) -> usize {
203 let r = self.resolution as usize;
204 z * r * r + y * r + x
205 }
206
207 pub fn strength_at(&self, x: usize, y: usize, z: usize) -> f32 {
208 self.strengths[self.index(x, y, z)]
209 }
210
211 pub fn position_at(&self, x: usize, y: usize, z: usize) -> Vec3 {
212 self.bounds_min + Vec3::new(x as f32, y as f32, z as f32) * self.step
213 }
214
215 pub fn color_at(&self, x: usize, y: usize, z: usize) -> Vec4 {
216 if self.colors.is_empty() { Vec4::ONE } else { self.colors[self.index(x, y, z)] }
217 }
218
219 pub fn emission_at(&self, x: usize, y: usize, z: usize) -> f32 {
220 if self.emissions.is_empty() { 0.0 } else { self.emissions[self.index(x, y, z)] }
221 }
222}
223
224fn evaluate_strength(entity: &MetaballEntity, point: Vec3) -> f32 {
226 let mut total = 0.0f32;
227 for source in &entity.sources {
228 if !source.is_active() { continue; }
229 total += source.evaluate(point, entity.hp_ratio);
230 }
231 total
232}
233
234#[derive(Debug, Clone)]
238pub struct DamageZone {
239 pub center: Vec3,
240 pub radius: f32,
241 pub intensity: f32,
242 pub time_remaining: f32,
243 pub total_duration: f32,
244}
245
246impl DamageZone {
247 pub fn new(center: Vec3, radius: f32, intensity: f32, duration: f32) -> Self {
248 Self { center, radius, intensity, time_remaining: duration, total_duration: duration }
249 }
250
251 pub fn evaluate(&self, point: Vec3) -> f32 {
253 let dist = (point - self.center).length();
254 if dist >= self.radius { return 0.0; }
255 let spatial = 1.0 - dist / self.radius;
256 let temporal = self.time_remaining / self.total_duration;
257 spatial * temporal * self.intensity
258 }
259
260 pub fn update(&mut self, dt: f32) -> bool {
262 self.time_remaining -= dt;
263 self.time_remaining > 0.0
264 }
265}
266
267#[cfg(test)]
270mod tests {
271 use super::*;
272 use super::super::entity_field::FieldSource;
273
274 fn test_entity() -> MetaballEntity {
275 let mut e = MetaballEntity::new(0.5, 8);
276 e.add_source(FieldSource::new(Vec3::ZERO, 1.0, 1.0));
277 e
278 }
279
280 #[test]
281 fn evaluator_strength_only() {
282 let e = test_entity();
283 let ev = FieldEvaluator::strength_only();
284 let sample = ev.evaluate(&e, Vec3::ZERO);
285 assert!(sample.strength > 0.0);
286 assert_eq!(sample.gradient, Vec3::ZERO); }
288
289 #[test]
290 fn evaluator_with_gradient() {
291 let e = test_entity();
292 let ev = FieldEvaluator::with_gradient();
293 let sample = ev.evaluate(&e, Vec3::new(0.5, 0.0, 0.0));
294 assert!(sample.gradient.length() > 0.0, "Gradient should be non-zero off-center");
295 }
296
297 #[test]
298 fn evaluator_full() {
299 let mut e = MetaballEntity::new(0.5, 8);
300 e.add_source(FieldSource::new(Vec3::ZERO, 1.0, 1.0).with_color(Vec4::new(1.0, 0.0, 0.0, 1.0)));
301 let ev = FieldEvaluator::full();
302 let sample = ev.evaluate(&e, Vec3::ZERO);
303 assert!(sample.color.x > 0.5); }
305
306 #[test]
307 fn sample_grid_correct_size() {
308 let e = test_entity();
309 let ev = FieldEvaluator::strength_only();
310 let grid = ev.sample_grid(&e, Vec3::splat(-2.0), Vec3::splat(2.0), 4);
311 assert_eq!(grid.strengths.len(), 4 * 4 * 4);
312 }
313
314 #[test]
315 fn damage_zone_decays() {
316 let mut zone = DamageZone::new(Vec3::ZERO, 1.0, 1.0, 1.0);
317 assert!(zone.evaluate(Vec3::ZERO) > 0.0);
318 zone.update(0.5);
319 let after = zone.evaluate(Vec3::ZERO);
320 assert!(after < 1.0);
321 zone.update(0.6);
322 assert!(!zone.update(0.0)); }
324
325 #[test]
326 fn grid_indexing() {
327 let e = test_entity();
328 let ev = FieldEvaluator::strength_only();
329 let grid = ev.sample_grid(&e, Vec3::splat(-1.0), Vec3::splat(1.0), 4);
330 let center_val = grid.strength_at(2, 2, 2);
331 let corner_val = grid.strength_at(0, 0, 0);
332 assert!(center_val > corner_val, "Center should be stronger than corner");
333 }
334}