1use gizmo_core::input::Input;
2use gizmo_core::World;
3use mlua::prelude::*;
4use mlua::RegistryKey;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::api_ai;
9use crate::api_audio;
10use crate::api_entity;
11use crate::api_fighter;
12use crate::api_input;
13use crate::api_physics;
14use crate::api_scene;
15use crate::api_time;
16use crate::api_vehicle;
17use crate::commands::{CommandQueue, ScriptCommand};
18
19pub struct ScriptEngine {
21 lua: Lua,
22 loaded_scripts: HashMap<String, (String, RegistryKey)>,
23 command_queue: Arc<CommandQueue>,
24 elapsed_time: f32,
25 pub log_queue: Arc<std::sync::Mutex<Vec<(String, String)>>>, }
28
29unsafe impl Send for ScriptEngine {}
30unsafe impl Sync for ScriptEngine {}
31
32impl std::fmt::Debug for ScriptEngine {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("ScriptEngine")
37 .field("lua", &"<Lua VM>")
38 .field("loaded_scripts", &self.loaded_scripts.keys())
39 .field("elapsed_time", &self.elapsed_time)
40 .field(
41 "queued_commands",
42 &self.command_queue.len(),
43 )
44 .field(
45 "queued_logs",
46 &self.log_queue.lock().map(|q| q.len()).unwrap_or(0),
47 )
48 .finish()
49 }
50}
51
52#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
54pub struct Script {
55 pub file_path: String,
56 #[serde(default, skip)]
57 pub initialized: bool, }
59
60impl Script {
61 pub fn new(path: &str) -> Self {
62 Self {
63 file_path: path.to_string(),
64 initialized: false,
65 }
66 }
67}
68
69#[derive(Clone, Debug, Default)]
71#[non_exhaustive]
72pub struct ScriptContext {
73 pub entity_id: u32,
74 pub dt: f32,
75 pub position: [f32; 3],
76 pub velocity: [f32; 3],
77 pub key_w: bool,
78 pub key_a: bool,
79 pub key_s: bool,
80 pub key_d: bool,
81 pub key_space: bool,
82 pub key_up: bool,
83 pub key_down: bool,
84 pub key_left: bool,
85 pub key_right: bool,
86}
87
88#[derive(Clone, Debug, Default)]
90pub struct ScriptResult {
91 pub new_position: Option<[f32; 3]>,
92 pub new_velocity: Option<[f32; 3]>,
93}
94
95impl ScriptEngine {
96 pub fn new() -> Result<Self, LuaError> {
97 let lua = Lua::new();
98 let command_queue = Arc::new(CommandQueue::new());
99 let log_queue = Arc::new(std::sync::Mutex::new(Vec::new()));
100
101 lua.globals().set("os", LuaNil)?;
103 lua.globals().set("io", LuaNil)?;
104 lua.globals().set("loadfile", LuaNil)?;
105 lua.globals().set("dofile", LuaNil)?;
106 lua.globals().set("require", LuaNil)?;
107 lua.globals().set("package", LuaNil)?;
108 lua.globals().set("debug", LuaNil)?;
109 lua.globals().set("loadstring", LuaNil)?;
110 lua.globals().set("load", LuaNil)?;
111
112 let lq_clone1 = log_queue.clone();
114 lua.globals().set(
115 "print_engine",
116 lua.create_function(move |_, msg: String| {
117 if let Ok(mut q) = lq_clone1.lock() {
118 q.push(("info".to_string(), msg));
119 }
120 Ok(())
121 })?,
122 )?;
123
124 let lq_clone2 = log_queue.clone();
126 lua.globals().set(
127 "print",
128 lua.create_function(move |_, values: LuaMultiValue| {
129 let parts: Vec<String> = values
130 .iter()
131 .map(|v| {
132 if let mlua::Value::String(s) = v {
133 s.to_str().unwrap_or("").to_string()
134 } else if let mlua::Value::Number(n) = v {
135 n.to_string()
136 } else if let mlua::Value::Integer(i) = v {
137 i.to_string()
138 } else if let mlua::Value::Boolean(b) = v {
139 b.to_string()
140 } else {
141 format!("{:?}", v)
142 }
143 })
144 .collect();
145 if let Ok(mut q) = lq_clone2.lock() {
146 q.push(("info".to_string(), parts.join("\t")));
147 }
148 Ok(())
149 })?,
150 )?;
151
152 lua.load(
154 r#"
155 function vec3(x, y, z)
156 return { x = x or 0, y = y or 0, z = z or 0 }
157 end
158
159 function vec3_add(a, b)
160 return vec3(a.x + b.x, a.y + b.y, a.z + b.z)
161 end
162
163 function vec3_sub(a, b)
164 return vec3(a.x - b.x, a.y - b.y, a.z - b.z)
165 end
166
167 function vec3_scale(v, s)
168 return vec3(v.x * s, v.y * s, v.z * s)
169 end
170
171 function vec3_length(v)
172 return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
173 end
174
175 function vec3_normalize(v)
176 local len = vec3_length(v)
177 if len > 0.0001 then
178 return vec3(v.x / len, v.y / len, v.z / len)
179 end
180 return vec3(0, 0, 0)
181 end
182
183 function vec3_dot(a, b)
184 return a.x * b.x + a.y * b.y + a.z * b.z
185 end
186
187 function vec3_cross(a, b)
188 return vec3(
189 a.y * b.z - a.z * b.y,
190 a.z * b.x - a.x * b.z,
191 a.x * b.y - a.y * b.x
192 )
193 end
194
195 function vec3_lerp(a, b, t)
196 return vec3(
197 a.x + (b.x - a.x) * t,
198 a.y + (b.y - a.y) * t,
199 a.z + (b.z - a.z) * t
200 )
201 end
202
203 function vec3_distance(a, b)
204 return vec3_length(vec3_sub(a, b))
205 end
206
207 -- Clamp utility
208 function clamp(value, min, max)
209 return math.max(min, math.min(max, value))
210 end
211
212 -- Lerp utility
213 function lerp(a, b, t)
214 return a + (b - a) * t
215 end
216 "#,
217 )
218 .exec()?;
219
220 api_entity::register_entity_api(&lua, command_queue.clone())?;
222 api_fighter::register_fighter_api(&lua, command_queue.clone())?;
223 api_input::register_input_api(&lua)?;
224 api_physics::register_physics_api(&lua, command_queue.clone())?;
225 api_scene::register_scene_api(&lua, command_queue.clone())?;
226 api_audio::register_audio_api(&lua, command_queue.clone())?;
227 api_time::register_time_api(&lua)?;
228 api_vehicle::register_vehicle_api(&lua, command_queue.clone())?;
229 api_ai::register_ai_api(&lua, command_queue.clone())?;
230
231 Ok(Self {
232 lua,
233 loaded_scripts: HashMap::new(),
234 command_queue,
235 elapsed_time: 0.0,
236 log_queue,
237 })
238 }
239
240 pub fn load_script(&mut self, path: &str) -> Result<(), String> {
241 let content = std::fs::read_to_string(path)
242 .map_err(|e| format!("Script okunamadı {}: {}", path, e))?;
243
244 let env = self.lua.create_table().map_err(|e| e.to_string())?;
245
246 let meta = self.lua.create_table().map_err(|e| e.to_string())?;
248 meta.set("__index", self.lua.globals())
249 .map_err(|e| e.to_string())?;
250 env.set_metatable(Some(meta));
251
252 self.lua
254 .load(&content)
255 .set_environment(env.clone())
256 .exec()
257 .map_err(|e| format!("Lua hata {}: {}", path, e))?;
258
259 let key = self
260 .lua
261 .create_registry_value(env)
262 .map_err(|e| e.to_string())?;
263
264 if let Some((_, old_key)) = self.loaded_scripts.insert(path.to_string(), (content, key)) {
266 let _ = self.lua.remove_registry_value(old_key);
267 }
268
269 tracing::info!("🔧 ScriptEngine: Yüklendi ve İzole Edildi → {}", path);
270 Ok(())
271 }
272
273 pub fn update(&mut self, world: &World, input: &Input, dt: f32) -> Result<(), String> {
275 self.elapsed_time += dt;
276
277 api_entity::update_entity_read_api(&self.lua, world)
279 .map_err(|e| format!("Entity API güncelleme hatası: {}", e))?;
280 api_fighter::update_fighter_read_api(&self.lua, world)
281 .map_err(|e| format!("Fighter API güncelleme hatası: {}", e))?;
282 api_input::update_input_api(&self.lua, input)
283 .map_err(|e| format!("Input API güncelleme hatası: {}", e))?;
284 api_scene::update_scene_api(&self.lua, world)
285 .map_err(|e| format!("Scene API güncelleme hatası: {}", e))?;
286 api_time::update_time_api(&self.lua, dt, self.elapsed_time, 1.0 / dt.max(0.0001))
287 .map_err(|e| format!("Time API güncelleme hatası: {}", e))?;
288 api_physics::update_physics_api(&self.lua, world)
289 .map_err(|e| format!("Physics API güncelleme hatası: {}", e))?;
290
291 let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
296 ctx_table.set("dt", dt).map_err(|e| e.to_string())?;
297 ctx_table
298 .set("elapsed", self.elapsed_time)
299 .map_err(|e| e.to_string())?;
300
301 for (path, (_, key)) in &self.loaded_scripts {
302 let env: mlua::Table = self.lua.registry_value(key).map_err(|e| e.to_string())?;
303 if let Ok(func) = env.get::<_, LuaFunction>("on_update") {
304 func.call::<_, ()>(ctx_table.clone())
305 .map_err(|e| format!("Lua on_update hatası ({}): {}", path, e))?;
306 }
307 }
308
309 Ok(())
310 }
311
312 pub fn update_entity(
314 &mut self,
315 entity_id: u32,
316 script_path: &str,
317 dt: f32,
318 ) -> Result<(), String> {
319 if let Some((_, key)) = self.loaded_scripts.get(script_path) {
320 let env: mlua::Table = self.lua.registry_value(key).map_err(|e| e.to_string())?;
321
322 if let Ok(func) = env.get::<_, LuaFunction>("on_entity_update") {
324 func.call::<_, ()>((entity_id, dt)).map_err(|e| {
325 format!(
326 "Lua on_entity_update hatası (entity {} mod {}): {}",
327 entity_id, script_path, e
328 )
329 })?;
330 }
331 }
332 Ok(())
333 }
334
335 pub fn flush_commands(&self, world: &mut World, dt: f32) -> Vec<ScriptCommand> {
337 let commands = self.command_queue.drain();
338 let mut unhandled = Vec::new();
339
340 for cmd in commands {
341 match cmd {
342 ScriptCommand::SetPosition(id, pos) => {
343 let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
344 if let Some(mut t) = transforms.get_mut(id) {
345 t.position = pos;
346 }
347 }
348 ScriptCommand::SetRotation(id, rot) => {
349 let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
350 if let Some(mut t) = transforms.get_mut(id) {
351 t.rotation = rot;
352 }
353 }
354 ScriptCommand::SetScale(id, scale) => {
355 let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
356 if let Some(mut t) = transforms.get_mut(id) {
357 t.scale = scale;
358 }
359 }
360 ScriptCommand::SetVelocity(id, vel) => {
361 let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
362 if let Some(mut v) = velocities.get_mut(id) {
363 v.linear = vel;
364 }
365 }
366 ScriptCommand::SetAngularVelocity(id, ang_vel) => {
367 let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
368 if let Some(mut v) = velocities.get_mut(id) {
369 v.angular = ang_vel;
370 }
371 }
372 ScriptCommand::ApplyForce(id, force) => {
373 let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
374 if let Some(rb) = rbs.get(id) {
375 if rb.mass > 0.0 {
376 let accel = force * (1.0 / rb.mass);
377 drop(rbs);
378 if world
381 .borrow::<gizmo_physics_rigid::components::Velocity>()
382 .get(id)
383 .is_none()
384 {
385 if let Some(e) = world.entity(id) {
386 world.add_component(
387 e,
388 gizmo_physics_rigid::components::Velocity::new(
389 gizmo_math::Vec3::ZERO,
390 ),
391 );
392 }
393 }
394 let mut vels =
395 world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
396 if let Some(mut v) = vels.get_mut(id) {
397 v.linear += accel * dt;
398 }
399 }
400 }
401 }
402 ScriptCommand::ApplyImpulse(id, impulse) => {
403 let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
404 if let Some(rb) = rbs.get(id) {
405 if rb.mass > 0.0 {
406 let delta_v = impulse * (1.0 / rb.mass);
407 drop(rbs);
408 if world
411 .borrow::<gizmo_physics_rigid::components::Velocity>()
412 .get(id)
413 .is_none()
414 {
415 if let Some(e) = world.entity(id) {
416 world.add_component(
417 e,
418 gizmo_physics_rigid::components::Velocity::new(
419 gizmo_math::Vec3::ZERO,
420 ),
421 );
422 }
423 }
424 let mut vels =
425 world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
426 if let Some(mut v) = vels.get_mut(id) {
427 v.linear += delta_v;
428 }
429 }
430 }
431 }
432 ScriptCommand::AddRigidBody {
433 id,
434 mass,
435 use_gravity,
436 } => {
437 let entity = world.entity(id);
438 if let Some(e) = entity {
439 let rb = gizmo_physics_rigid::components::RigidBody::new(mass, use_gravity);
440 world.add_component(e, rb);
441 if world
443 .borrow::<gizmo_physics_rigid::components::Velocity>()
444 .get(id)
445 .is_none()
446 {
447 world.add_component(
448 e,
449 gizmo_physics_rigid::components::Velocity::new(gizmo_math::Vec3::ZERO),
450 );
451 }
452 }
453 }
454 ScriptCommand::AddBoxCollider { id, hx, hy, hz } => {
455 let entity = world.entity(id);
456 if let Some(e) = entity {
457 let col =
458 gizmo_physics_core::Collider::aabb(gizmo_math::Vec3::new(hx, hy, hz));
459 world.add_component(e, col);
460 }
461 }
462 ScriptCommand::AddSphereCollider { id, radius } => {
463 let entity = world.entity(id);
464 if let Some(e) = entity {
465 let col = gizmo_physics_core::Collider::sphere(radius);
466 world.add_component(e, col);
467 }
468 }
469
470 ScriptCommand::SetVehicleEngineForce(_id, _force) => {}
471 ScriptCommand::SetVehicleSteering(_id, _angle) => {}
472 ScriptCommand::SetVehicleBrake(_id, _force) => {}
473
474 ScriptCommand::SpawnEntity { name, position } => {
475 let entity = world.spawn();
476 world.add_component(entity, gizmo_core::EntityName::new(&name));
477 world
478 .add_component(entity, gizmo_physics_core::Transform::new(position));
479 let msg = format!(
480 "Entity spawn: '{}' at ({:.1}, {:.1}, {:.1})",
481 name, position.x, position.y, position.z
482 );
483 if let Ok(mut q) = self.log_queue.lock() {
484 q.push(("info".to_string(), msg));
485 }
486 }
487 ScriptCommand::SpawnPrefab {
488 name,
489 prefab_type,
490 position,
491 } => {
492 let entity = world.spawn();
493 world.add_component(entity, gizmo_core::EntityName::new(&name));
494 world
495 .add_component(entity, gizmo_physics_core::Transform::new(position));
496 world.add_component(entity, gizmo_core::PrefabRequest(prefab_type.clone()));
497 }
498 ScriptCommand::DestroyEntity(id) => {
499 world.despawn_by_id(id);
500 if let Ok(mut q) = self.log_queue.lock() {
501 q.push(("info".to_string(), format!("Entity destroyed: {}", id)));
502 }
503 }
504ScriptCommand::SetEntityName(id, name) => {
505 let mut names = world.borrow_mut::<gizmo_core::EntityName>();
506 if let Some(mut n) = names.get_mut(id) {
507 n.0 = name;
508 }
509 }
510ScriptCommand::PlayAnimation { id, name, blend, loop_anim } => {
511 let mut players = world.borrow_mut::<gizmo_animation::skeletal::AnimationPlayer>();
512 if let Some(mut player) = players.get_mut(id) {
513 player.play_animation_by_name(&name, blend, loop_anim);
514 }
515 }
516 ScriptCommand::SetAnimationSpeed(id, speed) => {
517 let mut players = world.borrow_mut::<gizmo_animation::skeletal::AnimationPlayer>();
518 if let Some(mut player) = players.get_mut(id) {
519 player.speed = speed;
520 }
521 }
522 ScriptCommand::AddNavAgent(id) => {
523 let entity = world.entity(id);
524 if let Some(e) = entity {
525 world.add_component(e, gizmo_ai::components::NavAgent::default());
526 }
527 }
528 ScriptCommand::SetAiTarget(id, target) => {
529 let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
530 if let Some(mut agent) = agents.get_mut(id) {
531 agent.set_target(target);
532 }
533 }
534 ScriptCommand::ClearAiTarget(id) => {
535 let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
536 if let Some(mut agent) = agents.get_mut(id) {
537 agent.clear_target();
540 }
541 }
542 ScriptCommand::SetFighterMove { id, name, startup, active, recovery, damage } => {
543 let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
544 if let Some(mut fighter) = fighters.get_mut(id) {
545 let mut frame_data =
546 gizmo_physics_core::components::fighter::FrameData::default();
547 frame_data.startup = startup;
548 frame_data.active = active;
549 frame_data.recovery = recovery;
550 frame_data.damage = damage;
551 let mut combat_move =
552 gizmo_physics_core::components::fighter::CombatMove::default();
553 combat_move.name = name;
554 combat_move.frame_data = frame_data;
555 fighter.active_move = Some(combat_move);
556 fighter.current_move_frame = 0;
557 }
558 }
559 ScriptCommand::ApplyHitstop(id, frames) => {
560 let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
561 if let Some(mut fighter) = fighters.get_mut(id) {
562 fighter.apply_hitstop(frames);
563 }
564 }
565 ScriptCommand::ApplyHitstun(id, frames) => {
566 let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
567 if let Some(mut fighter) = fighters.get_mut(id) {
568 fighter.apply_hitstun(frames);
569 }
570 }
571 ScriptCommand::SaveScene(_)
572 | ScriptCommand::ShowDialogue { .. }
573 | ScriptCommand::HideDialogue
574 | ScriptCommand::TriggerCutscene(_)
575 | ScriptCommand::EndCutscene
576 | ScriptCommand::AddCheckpoint { .. }
577 | ScriptCommand::ActivateCheckpoint(_)
578 | ScriptCommand::StartRace
579 | ScriptCommand::FinishRace { .. }
580 | ScriptCommand::ResetRace
581 | ScriptCommand::SetCameraTarget(_)
582 | ScriptCommand::SetCameraFov(_)
583 | ScriptCommand::SetFightCamera { .. } => {
584 }
586 other => {
587 unhandled.push(other);
588 }
589 }
590 }
591
592 unhandled
593 }
594
595 pub fn get_pending_audio_scene_commands(&self) -> Vec<ScriptCommand> {
597 Vec::new()
600 }
601
602 pub fn reload_if_changed(&mut self, path: &str) -> Result<bool, String> {
604 let current =
605 std::fs::read_to_string(path).map_err(|e| format!("Script okunamadı: {}", e))?;
606
607 if let Some((cached_code, _)) = self.loaded_scripts.get(path) {
608 if *cached_code == current {
609 return Ok(false);
610 }
611 }
612
613 self.load_script(path)?;
614 Ok(true)
615 }
616
617 pub fn has_function(&self, path: &str, name: &str) -> bool {
619 if let Some((_, key)) = self.loaded_scripts.get(path) {
620 if let Ok(env) = self.lua.registry_value::<mlua::Table>(key) {
621 return env.get::<_, LuaFunction>(name).is_ok();
622 }
623 }
624 false
625 }
626
627 pub fn run_entity_update(
629 &self,
630 path: &str,
631 func_name: &str,
632 ctx: &ScriptContext,
633 ) -> Result<ScriptResult, String> {
634 let env: mlua::Table = if let Some((_, key)) = self.loaded_scripts.get(path) {
635 self.lua.registry_value(key).map_err(|e| e.to_string())?
636 } else {
637 return Err(format!("Script not loaded: {}", path));
638 };
639
640 let func: LuaFunction = match env.get(func_name) {
641 Ok(f) => f,
642 Err(_) => return Ok(ScriptResult::default()),
643 };
644
645 let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
646 ctx_table
647 .set("entity_id", ctx.entity_id)
648 .map_err(|e| e.to_string())?;
649 ctx_table.set("dt", ctx.dt).map_err(|e| e.to_string())?;
650 ctx_table
651 .set("elapsed", self.elapsed_time)
652 .map_err(|e| e.to_string())?;
653
654 let pos = self.lua.create_table().map_err(|e| e.to_string())?;
655 pos.set("x", ctx.position[0]).map_err(|e| e.to_string())?;
656 pos.set("y", ctx.position[1]).map_err(|e| e.to_string())?;
657 pos.set("z", ctx.position[2]).map_err(|e| e.to_string())?;
658 ctx_table.set("position", pos).map_err(|e| e.to_string())?;
659
660 let vel = self.lua.create_table().map_err(|e| e.to_string())?;
661 vel.set("x", ctx.velocity[0]).map_err(|e| e.to_string())?;
662 vel.set("y", ctx.velocity[1]).map_err(|e| e.to_string())?;
663 vel.set("z", ctx.velocity[2]).map_err(|e| e.to_string())?;
664 ctx_table.set("velocity", vel).map_err(|e| e.to_string())?;
665
666 let input = self.lua.create_table().map_err(|e| e.to_string())?;
667 input.set("w", ctx.key_w).map_err(|e| e.to_string())?;
668 input.set("a", ctx.key_a).map_err(|e| e.to_string())?;
669 input.set("s", ctx.key_s).map_err(|e| e.to_string())?;
670 input.set("d", ctx.key_d).map_err(|e| e.to_string())?;
671 input
672 .set("space", ctx.key_space)
673 .map_err(|e| e.to_string())?;
674 input.set("up", ctx.key_up).map_err(|e| e.to_string())?;
675 input.set("down", ctx.key_down).map_err(|e| e.to_string())?;
676 input.set("left", ctx.key_left).map_err(|e| e.to_string())?;
677 input
678 .set("right", ctx.key_right)
679 .map_err(|e| e.to_string())?;
680 ctx_table.set("input", input).map_err(|e| e.to_string())?;
681
682 let result_table: LuaTable = func
683 .call(ctx_table)
684 .map_err(|e| format!("Lua runtime: {}", e))?;
685
686 let mut result = ScriptResult::default();
687
688 if let Ok(pos) = result_table.get::<_, LuaTable>("position") {
689 let x: f32 = pos.get("x").unwrap_or(0.0);
690 let y: f32 = pos.get("y").unwrap_or(0.0);
691 let z: f32 = pos.get("z").unwrap_or(0.0);
692 result.new_position = Some([x, y, z]);
693 }
694
695 if let Ok(vel) = result_table.get::<_, LuaTable>("velocity") {
696 let x: f32 = vel.get("x").unwrap_or(0.0);
697 let y: f32 = vel.get("y").unwrap_or(0.0);
698 let z: f32 = vel.get("z").unwrap_or(0.0);
699 result.new_velocity = Some([x, y, z]);
700 }
701
702 Ok(result)
703 }
704
705 pub fn command_queue(&self) -> &Arc<CommandQueue> {
707 &self.command_queue
708 }
709}
710
711gizmo_core::impl_component!(Script);
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use gizmo_math::Vec3;
717 use gizmo_physics_rigid::components::{RigidBody, Velocity};
718
719 #[test]
723 fn on_update_hook_fires_from_script_env() {
724 let mut engine = ScriptEngine::new().unwrap();
725 let world = World::new();
726 let input = gizmo_core::input::Input::default();
727
728 let path = std::env::temp_dir()
729 .join("gizmo_on_update_test.lua")
730 .to_string_lossy()
731 .into_owned();
732 std::fs::write(&path, "function on_update(ctx)\n entity.spawn(\"bullet\", 0, 0, 0)\nend\n")
733 .unwrap();
734 engine.load_script(&path).expect("load_script");
735
736 let before = engine.command_queue().len();
737 engine.update(&world, &input, 1.0 / 60.0).expect("update");
738 let after = engine.command_queue().len();
739 let _ = std::fs::remove_file(&path);
740
741 assert!(
742 after > before,
743 "on_update must run and queue a spawn command (before={before}, after={after})"
744 );
745 }
746
747 #[test]
750 fn apply_force_creates_velocity_when_missing() {
751 let engine = ScriptEngine::new().unwrap();
752 let mut world = World::new();
753
754 let entity = world.spawn();
755 world.add_component(entity, RigidBody::new(2.0, false));
756 assert!(world.borrow::<Velocity>().get(entity.id()).is_none());
758
759 engine
760 .command_queue()
761 .push(ScriptCommand::ApplyForce(entity.id(), Vec3::new(4.0, 0.0, 0.0)));
762
763 let dt = 0.5_f32;
764 engine.flush_commands(&mut world, dt);
765
766 let vels = world.borrow::<Velocity>();
767 let v = vels
768 .get(entity.id())
769 .expect("Velocity ApplyForce tarafından oluşturulmalıydı");
770 assert!((v.linear.x - 1.0).abs() < 1e-5, "x hızı yanlış: {}", v.linear.x);
772 }
773
774 #[test]
777 fn apply_impulse_creates_velocity_when_missing() {
778 let engine = ScriptEngine::new().unwrap();
779 let mut world = World::new();
780
781 let entity = world.spawn();
782 world.add_component(entity, RigidBody::new(2.0, false));
783 assert!(world.borrow::<Velocity>().get(entity.id()).is_none());
784
785 engine
786 .command_queue()
787 .push(ScriptCommand::ApplyImpulse(entity.id(), Vec3::new(6.0, 0.0, 0.0)));
788
789 engine.flush_commands(&mut world, 0.016);
790
791 let vels = world.borrow::<Velocity>();
792 let v = vels
793 .get(entity.id())
794 .expect("Velocity ApplyImpulse tarafından oluşturulmalıydı");
795 assert!((v.linear.x - 3.0).abs() < 1e-5, "x hızı yanlış: {}", v.linear.x);
797 }
798}