1use ry_core::{ModuleError, ModuleResult, RyditModule};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11pub struct PhysicsModule;
13
14impl RyditModule for PhysicsModule {
15 fn name(&self) -> &'static str {
16 "physics"
17 }
18
19 fn version(&self) -> &'static str {
20 "0.7.3"
21 }
22
23 fn register(&self) -> HashMap<&'static str, &'static str> {
24 let mut cmds = HashMap::new();
25 cmds.insert("projectile", "Simulación de proyectil");
26 cmds.insert("nbody_2", "Simulación N-cuerpos (2 cuerpos)");
27 cmds.insert("nbody_simulate", "Simulación N-cuerpos (múltiples cuerpos)");
28 cmds
29 }
30
31 fn execute(&self, command: &str, params: Value) -> ModuleResult {
32 match command {
33 "projectile" => self.projectile(params),
34 "nbody_2" => self.nbody_2(params),
35 "nbody_simulate" => self.nbody_simulate(params),
36 _ => Err(ModuleError {
37 code: "UNKNOWN_COMMAND".to_string(),
38 message: format!("Comando desconocido: {}", command),
39 }),
40 }
41 }
42}
43
44impl PhysicsModule {
45 fn projectile(&self, params: Value) -> ModuleResult {
55 let arr = params.as_array().ok_or_else(|| ModuleError {
56 code: "INVALID_PARAMS".to_string(),
57 message: "Params must be an array".to_string(),
58 })?;
59
60 if arr.len() != 4 {
61 return Err(ModuleError {
62 code: "INVALID_PARAMS".to_string(),
63 message: "physics::projectile requires 4 params: x0, y0, v0, angle".to_string(),
64 });
65 }
66
67 let x0 = arr[0].as_f64().unwrap_or(0.0);
68 let y0 = arr[1].as_f64().unwrap_or(0.0);
69 let v0 = arr[2].as_f64().unwrap_or(0.0);
70 let angle = arr[3].as_f64().unwrap_or(0.0);
71
72 let rad = angle.to_radians();
73 let vx = v0 * rad.cos();
74 let vy = v0 * rad.sin();
75 let g = 9.81;
76
77 let flight_time = 2.0 * vy / g;
78 let max_height = (vy * vy) / (2.0 * g);
79 let range = vx * flight_time;
80
81 Ok(json!([
82 x0 + vx * flight_time, y0, flight_time, max_height, range ]))
88 }
89
90 fn nbody_2(&self, params: Value) -> ModuleResult {
101 let arr = params.as_array().ok_or_else(|| ModuleError {
102 code: "INVALID_PARAMS".to_string(),
103 message: "Params must be an array".to_string(),
104 })?;
105
106 if arr.len() != 7 {
107 return Err(ModuleError {
108 code: "INVALID_PARAMS".to_string(),
109 message: "physics::nbody_2 requires 7 params: m1, m2, x1, y1, x2, y2, G"
110 .to_string(),
111 });
112 }
113
114 let m1 = arr[0].as_f64().unwrap_or(0.0);
115 let m2 = arr[1].as_f64().unwrap_or(0.0);
116 let x1 = arr[2].as_f64().unwrap_or(0.0);
117 let y1 = arr[3].as_f64().unwrap_or(0.0);
118 let x2 = arr[4].as_f64().unwrap_or(0.0);
119 let y2 = arr[5].as_f64().unwrap_or(0.0);
120 let g = arr[6].as_f64().unwrap_or(6.674e-11);
121
122 let dx = x2 - x1;
123 let dy = y2 - y1;
124 let dist = (dx * dx + dy * dy).sqrt();
125
126 if dist > 0.001 {
127 let force = g * m1 * m2 / (dist * dist);
128 let fx = force * dx / dist;
129 let fy = force * dy / dist;
130
131 Ok(json!([fx, fy, -fx, -fy, dist]))
132 } else {
133 Ok(json!([0.0, 0.0, 0.0, 0.0, dist]))
134 }
135 }
136
137 pub fn nbody_simulate(&self, params: Value) -> ModuleResult {
151 let arr = params.as_array().ok_or_else(|| ModuleError {
152 code: "INVALID_PARAMS".to_string(),
153 message: "nbody_simulate requiere array de bodies + dt + G".to_string(),
154 })?;
155
156 if arr.len() < 3 {
157 return Err(ModuleError {
158 code: "INVALID_PARAMS".to_string(),
159 message: "nbody_simulate requiere [bodies, dt, G]".to_string(),
160 });
161 }
162
163 let bodies_arr = arr[0].as_array().ok_or_else(|| ModuleError {
164 code: "INVALID_PARAMS".to_string(),
165 message: "bodies debe ser array de [mass, x, y, vx, vy, is_static]".to_string(),
166 })?;
167
168 let dt = arr[1].as_f64().unwrap_or(0.016);
169 let g = arr[2].as_f64().unwrap_or(6.674e-11);
170
171 let n = bodies_arr.len();
172 if n == 0 {
173 return Ok(json!([]));
174 }
175
176 struct Body {
178 mass: f64, x: f64, y: f64, vx: f64, vy: f64, is_static: bool,
179 ax: f64, ay: f64,
180 }
181
182 let mut bodies: Vec<Body> = bodies_arr.iter().filter_map(|b| {
183 let a = b.as_array()?;
184 if a.len() < 6 { return None; }
185 Some(Body {
186 mass: a[0].as_f64()?,
187 x: a[1].as_f64()?,
188 y: a[2].as_f64()?,
189 vx: a[3].as_f64()?,
190 vy: a[4].as_f64()?,
191 is_static: a[5].as_f64().map(|v| v != 0.0).unwrap_or(false),
192 ax: 0.0, ay: 0.0,
193 })
194 }).collect();
195
196 for i in 0..n {
198 for j in (i + 1)..n {
199 let dx = bodies[j].x - bodies[i].x;
200 let dy = bodies[j].y - bodies[i].y;
201 let dist_sq = dx * dx + dy * dy;
202 let dist = dist_sq.sqrt();
203 if dist < 0.001 { continue; }
204 let force = g * bodies[i].mass * bodies[j].mass / dist_sq;
205 let ax = force * dx / (dist * bodies[i].mass);
206 let ay = force * dy / (dist * bodies[i].mass);
207 let ax_j = -force * dx / (dist * bodies[j].mass);
208 let ay_j = -force * dy / (dist * bodies[j].mass);
209 if !bodies[i].is_static { bodies[i].ax += ax; bodies[i].ay += ay; }
210 if !bodies[j].is_static { bodies[j].ax += ax_j; bodies[j].ay += ay_j; }
211 }
212 }
213
214 let result: Vec<Value> = bodies.iter().map(|b| {
216 let vx = if b.is_static { b.vx } else { b.vx + b.ax * dt };
217 let vy = if b.is_static { b.vy } else { b.vy + b.ay * dt };
218 let x = if b.is_static { b.x } else { b.x + vx * dt };
219 let y = if b.is_static { b.y } else { b.y + vy * dt };
220 json!([x, y, vx, vy])
221 }).collect();
222
223 Ok(json!(result))
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_physics_module_name() {
233 let module = PhysicsModule;
234 assert_eq!(module.name(), "physics");
235 assert_eq!(module.version(), "0.7.3");
236 }
237
238 #[test]
239 fn test_physics_register() {
240 let module = PhysicsModule;
241 let cmds = module.register();
242
243 assert!(cmds.contains_key("projectile"));
244 assert!(cmds.contains_key("nbody_2"));
245 }
246
247 #[test]
248 fn test_projectile() {
249 let module = PhysicsModule;
250 let params = json!([0.0, 0.0, 10.0, 45.0]);
252 let result = module.execute("projectile", params).unwrap();
253
254 let arr = result.as_array().unwrap();
255 assert_eq!(arr.len(), 5);
256 let flight_time = arr[2].as_f64().unwrap();
258 assert!(flight_time > 1.4 && flight_time < 1.5);
259 }
260
261 #[test]
262 fn test_nbody_2() {
263 let module = PhysicsModule;
264 let params = json!([100.0, 200.0, 0.0, 0.0, 10.0, 0.0, 1.0]);
266 let result = module.execute("nbody_2", params).unwrap();
267
268 let arr = result.as_array().unwrap();
269 assert_eq!(arr.len(), 5);
270 let fx = arr[0].as_f64().unwrap();
272 assert!((fx - 200.0).abs() < 0.01);
273 }
274
275 #[test]
276 fn test_nbody_2_close() {
277 let module = PhysicsModule;
278 let params = json!([100.0, 200.0, 0.0, 0.0, 0.0001, 0.0, 1.0]);
280 let result = module.execute("nbody_2", params).unwrap();
281
282 let arr = result.as_array().unwrap();
283 assert_eq!(arr[0].as_f64().unwrap(), 0.0);
284 assert_eq!(arr[1].as_f64().unwrap(), 0.0);
285 }
286
287 #[test]
288 fn test_unknown_command() {
289 let module = PhysicsModule;
290 let result = module.execute("unknown", json!([]));
291
292 assert!(result.is_err());
293 let err = result.unwrap_err();
294 assert_eq!(err.code, "UNKNOWN_COMMAND");
295 }
296}