1use crate::commands::{CommandQueue, ScriptCommand};
6use gizmo_core::World;
7use mlua::prelude::*;
8use std::sync::Arc;
9
10pub fn register_scene_api(lua: &Lua, command_queue: Arc<CommandQueue>) -> Result<(), LuaError> {
12 crate::api_table::register_protected(lua, "scene", |scene_table| {
13
14 {
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 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 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 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 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 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#[tracing::instrument(skip_all, name = "script_scene_read")]
209pub fn update_scene_api(lua: &Lua, world: &World) -> Result<(), LuaError> {
210 let scene_table = crate::api_table::raw(lua, "scene")?;
213
214 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 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 #[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 #[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 #[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 #[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}