Skip to main content

gizmo_scripting/
api_scene.rs

1//! Scene API — Lua'ya sunulan sahne ve oyun yönetim fonksiyonları
2//!
3//! Kapsam: sahne kaydet/yükle, diyalog, ara sahne, yarış sistemi, kamera.
4
5use crate::commands::{CommandQueue, ScriptCommand};
6use gizmo_core::World;
7use mlua::prelude::*;
8use std::sync::Arc;
9
10/// Scene + Game API fonksiyonlarını Lua'ya kaydeder
11pub fn register_scene_api(lua: &Lua, command_queue: Arc<CommandQueue>) -> Result<(), LuaError> {
12    crate::api_table::register_protected(lua, "scene", |scene_table| {
13
14    // --- SAHNE KAYDET / YÜKLE ---
15    {
16        let cq = command_queue.clone();
17        scene_table.raw_set(
18            "save",
19            lua.create_function(move |_, path: String| {
20                cq.push(ScriptCommand::SaveScene(path));
21                Ok(())
22            })?,
23        )?;
24    }
25    {
26        let cq = command_queue.clone();
27        scene_table.raw_set(
28            "load",
29            lua.create_function(move |_, path: String| {
30                cq.push(ScriptCommand::LoadScene(path));
31                Ok(())
32            })?,
33        )?;
34    }
35
36        // Defined once, at registration — the update path re-loaded them every frame, which the
37        // comment there called "idempotent" and which the read-only proxy now rejects outright.
38        lua.load(
39            r#"
40            function scene.get_all_entities()
41                return scene._entities or {}
42            end
43
44            function scene.find_by_name(name)
45                return scene._name_map[name]
46            end
47
48            function scene.entity_count()
49                local count = 0
50                for _ in pairs(scene._entities or {}) do count = count + 1 end
51                return count
52            end
53        "#,
54        )
55        .exec()?;
56
57        Ok(())
58    })?;
59
60    // --- DİYALOG ---
61    crate::api_table::register_protected(lua, "dialogue", |dialogue_table| {
62    {
63        let cq = command_queue.clone();
64        dialogue_table.set(
65            "show",
66            lua.create_function(
67                move |_, (speaker, text, duration): (String, String, Option<f32>)| {
68                    cq.push(ScriptCommand::ShowDialogue {
69                        speaker,
70                        text,
71                        duration: duration.unwrap_or(3.0),
72                    });
73                    Ok(())
74                },
75            )?,
76        )?;
77    }
78    {
79        let cq = command_queue.clone();
80        dialogue_table.set(
81            "hide",
82            lua.create_function(move |_, ()| {
83                cq.push(ScriptCommand::HideDialogue);
84                Ok(())
85            })?,
86        )?;
87    }
88        Ok(())
89    })?;
90
91    // --- ARA SAHNE (CUTSCENE) ---
92    crate::api_table::register_protected(lua, "cutscene", |cutscene_table| {
93    {
94        let cq = command_queue.clone();
95        cutscene_table.set(
96            "play",
97            lua.create_function(move |_, name: String| {
98                cq.push(ScriptCommand::TriggerCutscene(name));
99                Ok(())
100            })?,
101        )?;
102    }
103    {
104        let cq = command_queue.clone();
105        cutscene_table.set(
106            "stop",
107            lua.create_function(move |_, ()| {
108                cq.push(ScriptCommand::EndCutscene);
109                Ok(())
110            })?,
111        )?;
112    }
113        Ok(())
114    })?;
115
116    // --- YARIŞ SİSTEMİ ---
117    crate::api_table::register_protected(lua, "race", |race_table| {
118    {
119        let cq = command_queue.clone();
120        race_table.set(
121            "add_checkpoint",
122            lua.create_function(
123                move |_, (id, x, y, z, radius): (u32, f32, f32, f32, Option<f32>)| {
124                    cq.push(ScriptCommand::AddCheckpoint {
125                        id,
126                        position: gizmo_math::Vec3::new(x, y, z),
127                        radius: radius.unwrap_or(5.0),
128                    });
129                    Ok(())
130                },
131            )?,
132        )?;
133    }
134    {
135        let cq = command_queue.clone();
136        race_table.set(
137            "activate_checkpoint",
138            lua.create_function(move |_, id: u32| {
139                cq.push(ScriptCommand::ActivateCheckpoint(id));
140                Ok(())
141            })?,
142        )?;
143    }
144    {
145        let cq = command_queue.clone();
146        race_table.set(
147            "finish",
148            lua.create_function(move |_, winner: String| {
149                cq.push(ScriptCommand::FinishRace {
150                    winner_name: winner,
151                });
152                Ok(())
153            })?,
154        )?;
155    }
156    {
157        let cq = command_queue.clone();
158        race_table.set(
159            "reset",
160            lua.create_function(move |_, ()| {
161                cq.push(ScriptCommand::ResetRace);
162                Ok(())
163            })?,
164        )?;
165    }
166    {
167        let cq = command_queue.clone();
168        race_table.set(
169            "start",
170            lua.create_function(move |_, ()| {
171                cq.push(ScriptCommand::StartRace);
172                Ok(())
173            })?,
174        )?;
175    }
176        Ok(())
177    })?;
178
179    // --- KAMERA ---
180    crate::api_table::register_protected(lua, "camera", |camera_table| {
181    {
182        let cq = command_queue.clone();
183        camera_table.set(
184            "follow",
185            lua.create_function(move |_, entity_id: u32| {
186                cq.push(ScriptCommand::SetCameraTarget(entity_id));
187                Ok(())
188            })?,
189        )?;
190    }
191    {
192        let cq = command_queue.clone();
193        camera_table.set(
194            "set_fov",
195            lua.create_function(move |_, fov: f32| {
196                cq.push(ScriptCommand::SetCameraFov(fov));
197                Ok(())
198            })?,
199        )?;
200    }
201        Ok(())
202    })?;
203
204    Ok(())
205}
206
207/// Her frame sahne verisini Lua'ya günceller (entity listesi, isim arama)
208#[tracing::instrument(skip_all, name = "script_scene_read")]
209pub fn update_scene_api(lua: &Lua, world: &World) -> Result<(), LuaError> {
210    // The real table, not the global: the global is a read-only proxy so a script cannot
211    // rewrite the API (see `api_table`), and the engine's per-frame writes go behind it.
212    let scene_table = crate::api_table::raw(lua, "scene")?;
213
214    // Entity listesini güncelle
215    let entities_table = lua.create_table()?;
216    for (idx, entity) in world.iter_alive_entities().into_iter().enumerate() {
217        entities_table.set(idx + 1, entity.id())?;
218    }
219    scene_table.raw_set("_entities", entities_table)?;
220
221    // İsim → ID eşleme tablosu
222    let name_map = lua.create_table()?;
223    let names = world.borrow::<gizmo_core::EntityName>();
224    for (eid, _) in names.iter() {
225        if let Some(n) = names.get(eid) {
226            name_map.set(n.0.clone(), eid)?;
227        }
228    }
229    scene_table.raw_set("_name_map", name_map)?;
230
231
232    Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use mlua::Lua;
239
240    fn setup() -> (Lua, Arc<CommandQueue>) {
241        let lua = Lua::new();
242        let cq = Arc::new(CommandQueue::new());
243        register_scene_api(&lua, cq.clone()).unwrap();
244        (lua, cq)
245    }
246
247    /// dialogue.show süre argümanı opsiyonel: verilmezse 3.0'a düşmeli, verilirse korunmalı.
248    #[test]
249    fn dialogue_show_duration_defaults_and_overrides() {
250        let (lua, cq) = setup();
251        lua.load(r#"dialogue.show("Ada", "merhaba")"#).exec().unwrap();
252        lua.load(r#"dialogue.show("Ada", "hoşça kal", 1.5)"#).exec().unwrap();
253
254        let cmds = cq.drain();
255        assert_eq!(cmds.len(), 2);
256        match &cmds[0] {
257            ScriptCommand::ShowDialogue { speaker, text, duration } => {
258                assert_eq!(speaker, "Ada");
259                assert_eq!(text, "merhaba");
260                assert!((duration - 3.0).abs() < 1e-6, "varsayılan süre 3.0 olmalı");
261            }
262            other => panic!("beklenen ShowDialogue, gelen {other:?}"),
263        }
264        match &cmds[1] {
265            ScriptCommand::ShowDialogue { duration, .. } => {
266                assert!((duration - 1.5).abs() < 1e-6, "verilen süre korunmalı");
267            }
268            other => panic!("beklenen ShowDialogue, gelen {other:?}"),
269        }
270    }
271
272    /// race.add_checkpoint yarıçapı opsiyonel: verilmezse 5.0'a düşmeli, verilirse korunmalı.
273    #[test]
274    fn checkpoint_radius_defaults_and_overrides() {
275        let (lua, cq) = setup();
276        lua.load("race.add_checkpoint(1, 0.0, 0.0, 0.0)").exec().unwrap();
277        lua.load("race.add_checkpoint(2, 1.0, 2.0, 3.0, 12.0)").exec().unwrap();
278
279        let cmds = cq.drain();
280        assert_eq!(cmds.len(), 2);
281        match &cmds[0] {
282            ScriptCommand::AddCheckpoint { id, position, radius } => {
283                assert_eq!(*id, 1);
284                assert_eq!(*position, gizmo_math::Vec3::ZERO);
285                assert!((radius - 5.0).abs() < 1e-6, "varsayılan yarıçap 5.0 olmalı");
286            }
287            other => panic!("beklenen AddCheckpoint, gelen {other:?}"),
288        }
289        match &cmds[1] {
290            ScriptCommand::AddCheckpoint { position, radius, .. } => {
291                assert_eq!(*position, gizmo_math::Vec3::new(1.0, 2.0, 3.0));
292                assert!((radius - 12.0).abs() < 1e-6, "verilen yarıçap korunmalı");
293            }
294            other => panic!("beklenen AddCheckpoint, gelen {other:?}"),
295        }
296    }
297
298    /// Sahne/kamera/ara-sahne binding'leri: her çağrı beklenen komutu üretmeli.
299    #[test]
300    fn scene_camera_cutscene_calls_push_expected_commands() {
301        let (lua, cq) = setup();
302        lua.load(
303            r#"
304            scene.save("slot1.scene")
305            scene.load("level2.scene")
306            camera.follow(7)
307            camera.set_fov(75.0)
308            cutscene.play("intro")
309            cutscene.stop()
310            race.start()
311            race.activate_checkpoint(3)
312            race.finish("Ada")
313            race.reset()
314            "#,
315        )
316        .exec()
317        .unwrap();
318
319        let cmds = cq.drain();
320        assert!(matches!(&cmds[0], ScriptCommand::SaveScene(p) if p == "slot1.scene"));
321        assert!(matches!(&cmds[1], ScriptCommand::LoadScene(p) if p == "level2.scene"));
322        assert!(matches!(cmds[2], ScriptCommand::SetCameraTarget(7)));
323        assert!(matches!(cmds[3], ScriptCommand::SetCameraFov(f) if (f - 75.0).abs() < 1e-6));
324        assert!(matches!(&cmds[4], ScriptCommand::TriggerCutscene(n) if n == "intro"));
325        assert!(matches!(cmds[5], ScriptCommand::EndCutscene));
326        assert!(matches!(cmds[6], ScriptCommand::StartRace));
327        assert!(matches!(cmds[7], ScriptCommand::ActivateCheckpoint(3)));
328        assert!(matches!(&cmds[8], ScriptCommand::FinishRace { winner_name } if winner_name == "Ada"));
329        assert!(matches!(cmds[9], ScriptCommand::ResetRace));
330    }
331
332    /// update_scene_api isim→id eşlemesi kurmalı; bilinmeyen isim nil, entity_count doğru.
333    #[test]
334    fn scene_name_lookup_and_count() {
335        let (lua, cq) = setup();
336        let _ = cq;
337        let mut world = World::new();
338        let a = world.spawn();
339        world.add_component(a, gizmo_core::EntityName::new("player"));
340        let b = world.spawn();
341        world.add_component(b, gizmo_core::EntityName::new("enemy"));
342
343        update_scene_api(&lua, &world).unwrap();
344
345        let a_id = a.id();
346        let b_id = b.id();
347        lua.load(format!(
348            r#"
349            assert(scene.find_by_name("player") == {a_id}, "player id eşleşmeli")
350            assert(scene.find_by_name("enemy") == {b_id}, "enemy id eşleşmeli")
351            assert(scene.find_by_name("ghost") == nil, "bilinmeyen isim nil dönmeli")
352            assert(scene.entity_count() == 2, "iki canlı entity sayılmalı")
353            "#
354        ))
355        .exec()
356        .unwrap();
357    }
358}