Skip to main content

gizmo_scripting/
api_fighter.rs

1//! Fighter API — Lua'ya sunulan dövüş sistemi fonksiyonları
2//!
3//! Lua scriptlerinden kombo sorgulama, hitstop/hitstun uygulama ve saldırı başlatma için kullanılır.
4
5use crate::commands::{CommandQueue, ScriptCommand};
6use gizmo_core::World;
7use mlua::prelude::*;
8use std::sync::Arc;
9
10pub fn register_fighter_api(lua: &Lua, command_queue: Arc<CommandQueue>) -> Result<(), LuaError> {
11    let fighter_table = lua.create_table()?;
12
13    // Oku-Yaz tablosu
14    fighter_table.set("_buffers", lua.create_table()?)?;
15    fighter_table.set("_is_locked", lua.create_table()?)?;
16
17    // === SET FIGHTER MOVE ===
18    {
19        let cq = command_queue.clone();
20        fighter_table.set(
21            "set_move",
22            lua.create_function(
23                move |_, (id, name, startup, active, recovery, damage): (u32, String, u32, u32, u32, f32)| {
24                    cq.push(ScriptCommand::SetFighterMove {
25                        id,
26                        name,
27                        startup,
28                        active,
29                        recovery,
30                        damage,
31                    });
32                    Ok(())
33                },
34            )?,
35        )?;
36    }
37
38    // === APPLY HITSTOP ===
39    {
40        let cq = command_queue.clone();
41        fighter_table.set(
42            "apply_hitstop",
43            lua.create_function(move |_, (id, frames): (u32, u32)| {
44                cq.push(ScriptCommand::ApplyHitstop(id, frames));
45                Ok(())
46            })?,
47        )?;
48    }
49
50    // === APPLY HITSTUN ===
51    {
52        let cq = command_queue.clone();
53        fighter_table.set(
54            "apply_hitstun",
55            lua.create_function(move |_, (id, frames): (u32, u32)| {
56                cq.push(ScriptCommand::ApplyHitstun(id, frames));
57                Ok(())
58            })?,
59        )?;
60    }
61
62    lua.globals().set("fighter", fighter_table)?;
63
64    // Lua tarafında kombo kontrol eden yardımcı fonksiyon
65    lua.load(
66        r#"
67        function fighter.is_locked(id)
68            return fighter._is_locked[id] or false
69        end
70
71        function fighter.check_combo(id, combo, max_gap)
72            local buffer = fighter._buffers[id]
73            if not buffer then return false end
74
75            local combo_idx = #combo
76            if combo_idx == 0 then return false end
77
78            local gap_counter = 0
79            
80            for i = 1, #buffer do
81                local frame = buffer[i]
82                local target_input = combo[combo_idx]
83
84                if frame.just_pressed[target_input] then
85                    combo_idx = combo_idx - 1
86                    gap_counter = 0
87                    if combo_idx == 0 then
88                        return true
89                    end
90                elseif gap_counter >= max_gap then
91                    return false
92                else
93                    gap_counter = gap_counter + 1
94                end
95            end
96            
97            return false
98        end
99    "#,
100    )
101    .exec()?;
102
103    Ok(())
104}
105
106pub fn update_fighter_read_api(lua: &Lua, world: &World) -> Result<(), LuaError> {
107    let fighter_table: LuaTable = lua.globals().get("fighter")?;
108
109    let buffers = lua.create_table()?;
110    let is_locked = lua.create_table()?;
111
112    let controllers = world.borrow::<gizmo_physics_core::components::FighterController>();
113    for (eid, _) in controllers.iter() {
114        if let Some(fighter) = controllers.get(eid) {
115            is_locked.set(eid, fighter.is_locked())?;
116
117            let frames_table = lua.create_table()?;
118            for (i, frame) in fighter.input_buffer.frames.iter().enumerate() {
119                let frame_table = lua.create_table()?;
120                
121                let jp_table = lua.create_table()?;
122                for k in &frame.just_pressed {
123                    jp_table.set(k.clone(), true)?;
124                }
125                
126                let p_table = lua.create_table()?;
127                for k in &frame.pressed {
128                    p_table.set(k.clone(), true)?;
129                }
130                
131                frame_table.set("just_pressed", jp_table)?;
132                frame_table.set("pressed", p_table)?;
133                
134                frames_table.set(i + 1, frame_table)?;
135            }
136            buffers.set(eid, frames_table)?;
137        }
138    }
139
140    fighter_table.set("_buffers", buffers)?;
141    fighter_table.set("_is_locked", is_locked)?;
142
143    Ok(())
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::commands::CommandQueue;
150
151    /// Belirtilen frame indekslerinde `input` tuşunu just_pressed olarak
152    /// işaretleyen bir buffer'ı Lua'ya kuran yardımcı ve check_combo sonucu.
153    fn run_combo(setup: &str) -> bool {
154        let lua = Lua::new();
155        register_fighter_api(&lua, Arc::new(CommandQueue::new())).unwrap();
156        lua.load(setup).exec().unwrap();
157        lua.load("return fighter.check_combo(1, combo, max_gap)")
158            .eval()
159            .unwrap()
160    }
161
162    /// Regression: max_gap=2 tam olarak 2 frame boşluğa izin vermeli.
163    /// 'b' bulunduktan sonra 2 boş frame + 'a' => kabul.
164    /// 3 boş frame => ret.
165    #[test]
166    fn combo_gap_boundary_is_exact() {
167        // Buffer ileri taranır; önce combo'nun son elemanı ('b') aranır.
168        // frame1='b', frame2/3 boş, frame4='a' -> 2 boşluk, kabul edilmeli.
169        let accepted = run_combo(
170            r#"
171            combo = { "a", "b" }
172            max_gap = 2
173            local function f(k) return { just_pressed = k and { [k] = true } or {} } end
174            fighter._buffers[1] = { f("b"), f(nil), f(nil), f("a") }
175            "#,
176        );
177        assert!(accepted, "2 frame boşluk max_gap=2 için kabul edilmeli");
178
179        // frame1='b', frame2/3/4 boş, frame5='a' -> 3 boşluk, reddedilmeli.
180        let rejected = run_combo(
181            r#"
182            combo = { "a", "b" }
183            max_gap = 2
184            local function f(k) return { just_pressed = k and { [k] = true } or {} } end
185            fighter._buffers[1] = { f("b"), f(nil), f(nil), f(nil), f("a") }
186            "#,
187        );
188        assert!(
189            !rejected,
190            "3 frame boşluk max_gap=2 için REDDEDİLMELİ (off-by-one)"
191        );
192    }
193}