use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Clone, Serialize, Deserialize)]
pub struct Input {
keys_pressed: HashSet<u32>, keys_just_pressed: HashSet<u32>, keys_just_released: HashSet<u32>,
mouse_buttons_pressed: HashSet<u32>,
mouse_buttons_just_pressed: HashSet<u32>,
mouse_buttons_just_released: HashSet<u32>,
mouse_position: (f32, f32),
mouse_delta: (f32, f32),
mouse_scroll_delta: f32,
}
impl Input {
pub fn new() -> Self {
Self {
keys_pressed: HashSet::new(),
keys_just_pressed: HashSet::new(),
keys_just_released: HashSet::new(),
mouse_buttons_pressed: HashSet::new(),
mouse_buttons_just_pressed: HashSet::new(),
mouse_buttons_just_released: HashSet::new(),
mouse_position: (0.0, 0.0),
mouse_delta: (0.0, 0.0),
mouse_scroll_delta: 0.0,
}
}
pub fn begin_frame(&mut self) {
for k in &self.keys_just_released {
self.keys_pressed.remove(k);
}
for b in &self.mouse_buttons_just_released {
self.mouse_buttons_pressed.remove(b);
}
self.keys_just_pressed.clear();
self.keys_just_released.clear();
self.mouse_buttons_just_pressed.clear();
self.mouse_buttons_just_released.clear();
self.mouse_delta = (0.0, 0.0);
self.mouse_scroll_delta = 0.0;
}
pub fn pressed_keys(&self) -> Vec<u32> {
self.keys_pressed.iter().copied().collect()
}
pub fn on_key_pressed(&mut self, key: u32) {
if self.keys_pressed.insert(key) {
self.keys_just_pressed.insert(key);
}
}
pub fn on_key_released(&mut self, key: u32) {
self.keys_just_released.insert(key);
if !self.keys_just_pressed.contains(&key) {
self.keys_pressed.remove(&key);
}
}
#[inline]
pub fn is_key_pressed(&self, key: u32) -> bool {
self.keys_pressed.contains(&key)
}
#[inline]
pub fn is_key_just_pressed(&self, key: u32) -> bool {
self.keys_just_pressed.contains(&key)
}
#[inline]
pub fn is_key_just_released(&self, key: u32) -> bool {
self.keys_just_released.contains(&key)
}
pub fn on_mouse_button_pressed(&mut self, button: u32) {
if self.mouse_buttons_pressed.insert(button) {
self.mouse_buttons_just_pressed.insert(button);
}
}
pub fn on_mouse_button_released(&mut self, button: u32) {
self.mouse_buttons_just_released.insert(button);
if !self.mouse_buttons_just_pressed.contains(&button) {
self.mouse_buttons_pressed.remove(&button);
}
}
#[inline]
pub fn is_mouse_button_pressed(&self, button: u32) -> bool {
self.mouse_buttons_pressed.contains(&button)
}
#[inline]
pub fn is_mouse_button_just_pressed(&self, button: u32) -> bool {
self.mouse_buttons_just_pressed.contains(&button)
}
#[inline]
pub fn is_mouse_button_just_released(&self, button: u32) -> bool {
self.mouse_buttons_just_released.contains(&button)
}
pub fn on_mouse_moved(&mut self, x: f32, y: f32) {
self.mouse_delta.0 += x - self.mouse_position.0;
self.mouse_delta.1 += y - self.mouse_position.1;
self.mouse_position = (x, y);
}
pub fn on_mouse_delta(&mut self, dx: f32, dy: f32) {
self.mouse_delta.0 += dx;
self.mouse_delta.1 += dy;
}
#[inline]
pub fn mouse_position(&self) -> (f32, f32) {
self.mouse_position
}
#[inline]
pub fn mouse_delta(&self) -> (f32, f32) {
self.mouse_delta
}
pub fn on_mouse_scroll(&mut self, delta: f32) {
self.mouse_scroll_delta += delta;
}
#[inline]
pub fn mouse_scroll(&self) -> f32 {
self.mouse_scroll_delta
}
}
impl Default for Input {
fn default() -> Self {
Self::new()
}
}
pub mod mouse {
pub const LEFT: u32 = 0;
pub const RIGHT: u32 = 1;
pub const MIDDLE: u32 = 2;
}
use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum InputBinding {
Key(u32),
MouseButton(u32),
}
#[derive(Clone)]
pub struct ActionMap {
bindings: HashMap<String, Vec<InputBinding>>,
}
impl ActionMap {
pub fn new() -> Self {
Self {
bindings: HashMap::new(),
}
}
pub fn bind_key(&mut self, action_name: &str, keycode: u32) {
self.bindings
.entry(action_name.to_string())
.or_default()
.push(InputBinding::Key(keycode));
}
pub fn bind_mouse_button(&mut self, action_name: &str, button: u32) {
self.bindings
.entry(action_name.to_string())
.or_default()
.push(InputBinding::MouseButton(button));
}
pub fn bind_action(&mut self, action_name: &str, keycode: u32) {
self.bind_key(action_name, keycode);
}
pub fn is_action_pressed(&self, input: &Input, action_name: &str) -> bool {
if let Some(bindings) = self.bindings.get(action_name) {
for binding in bindings {
match binding {
InputBinding::Key(k) => {
if input.is_key_pressed(*k) {
return true;
}
}
InputBinding::MouseButton(b) => {
if input.is_mouse_button_pressed(*b) {
return true;
}
}
}
}
}
false
}
pub fn is_action_just_pressed(&self, input: &Input, action_name: &str) -> bool {
if let Some(bindings) = self.bindings.get(action_name) {
for binding in bindings {
match binding {
InputBinding::Key(k) => {
if input.is_key_just_pressed(*k) {
return true;
}
}
InputBinding::MouseButton(b) => {
if input.is_mouse_button_just_pressed(*b) {
return true;
}
}
}
}
}
false
}
pub fn is_action_just_released(&self, input: &Input, action_name: &str) -> bool {
if let Some(bindings) = self.bindings.get(action_name) {
for binding in bindings {
match binding {
InputBinding::Key(k) => {
if input.is_key_just_released(*k) {
return true;
}
}
InputBinding::MouseButton(b) => {
if input.is_mouse_button_just_released(*b) {
return true;
}
}
}
}
}
false
}
}
impl Default for ActionMap {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fast_tap_preserves_pressed_for_one_frame() {
let mut input = Input::new();
input.on_key_pressed(42);
input.on_key_released(42);
assert!(input.is_key_pressed(42), "fast-tap: tuş pressed olmalı");
assert!(
input.is_key_just_pressed(42),
"fast-tap: tuş just_pressed olmalı"
);
assert!(
input.is_key_just_released(42),
"fast-tap: tuş just_released olmalı"
);
input.begin_frame();
assert!(
!input.is_key_pressed(42),
"sonraki frame: pressed false olmalı"
);
assert!(
!input.is_key_just_pressed(42),
"sonraki frame: just_pressed false olmalı"
);
assert!(
!input.is_key_just_released(42),
"sonraki frame: just_released false olmalı"
);
}
#[test]
fn test_normal_press_release_across_frames() {
let mut input = Input::new();
input.on_key_pressed(10);
assert!(input.is_key_pressed(10));
assert!(input.is_key_just_pressed(10));
input.begin_frame();
assert!(input.is_key_pressed(10));
assert!(!input.is_key_just_pressed(10));
input.on_key_released(10);
assert!(!input.is_key_pressed(10)); assert!(input.is_key_just_released(10));
input.begin_frame();
assert!(!input.is_key_pressed(10));
assert!(!input.is_key_just_released(10));
}
#[test]
fn test_fast_tap_mouse_button() {
let mut input = Input::new();
input.on_mouse_button_pressed(mouse::LEFT);
input.on_mouse_button_released(mouse::LEFT);
assert!(input.is_mouse_button_pressed(mouse::LEFT));
assert!(input.is_mouse_button_just_pressed(mouse::LEFT));
assert!(input.is_mouse_button_just_released(mouse::LEFT));
input.begin_frame();
assert!(!input.is_mouse_button_pressed(mouse::LEFT));
assert!(!input.is_mouse_button_just_pressed(mouse::LEFT));
assert!(!input.is_mouse_button_just_released(mouse::LEFT));
}
#[test]
fn test_mouse_moved_accumulates_delta() {
let mut input = Input::new();
input.on_mouse_moved(100.0, 200.0);
assert_eq!(input.mouse_delta(), (100.0, 200.0));
input.on_mouse_moved(150.0, 250.0);
assert_eq!(input.mouse_delta(), (150.0, 250.0));
assert_eq!(input.mouse_position(), (150.0, 250.0));
}
#[test]
fn test_mouse_delta_resets_on_begin_frame() {
let mut input = Input::new();
input.on_mouse_moved(100.0, 200.0);
assert_ne!(input.mouse_delta(), (0.0, 0.0));
input.begin_frame();
assert_eq!(input.mouse_delta(), (0.0, 0.0));
assert_eq!(input.mouse_position(), (100.0, 200.0));
}
#[test]
fn test_scroll_accumulates_and_resets() {
let mut input = Input::new();
input.on_mouse_scroll(3.0);
input.on_mouse_scroll(-1.0);
assert_eq!(input.mouse_scroll(), 2.0);
input.begin_frame();
assert_eq!(input.mouse_scroll(), 0.0);
}
#[test]
fn test_pressed_keys() {
let mut input = Input::new();
input.on_key_pressed(1);
input.on_key_pressed(2);
input.on_key_pressed(3);
let mut keys = input.pressed_keys();
keys.sort();
assert_eq!(keys, vec![1, 2, 3]);
}
#[test]
fn test_action_map_key_binding() {
let mut input = Input::new();
let mut actions = ActionMap::new();
actions.bind_key("Jump", 42);
input.on_key_pressed(42);
assert!(actions.is_action_pressed(&input, "Jump"));
assert!(actions.is_action_just_pressed(&input, "Jump"));
}
#[test]
fn test_action_map_mouse_binding() {
let mut input = Input::new();
let mut actions = ActionMap::new();
actions.bind_mouse_button("Attack", mouse::LEFT);
input.on_mouse_button_pressed(mouse::LEFT);
assert!(actions.is_action_pressed(&input, "Attack"));
assert!(actions.is_action_just_pressed(&input, "Attack"));
input.begin_frame();
input.on_mouse_button_released(mouse::LEFT);
assert!(actions.is_action_just_released(&input, "Attack"));
}
#[test]
fn test_action_map_mixed_bindings() {
let mut input = Input::new();
let mut actions = ActionMap::new();
actions.bind_key("Fire", 42);
actions.bind_mouse_button("Fire", mouse::LEFT);
assert!(!actions.is_action_pressed(&input, "Fire"));
input.on_mouse_button_pressed(mouse::LEFT);
assert!(actions.is_action_pressed(&input, "Fire"));
input.begin_frame();
input.on_mouse_button_released(mouse::LEFT);
input.on_key_pressed(42);
assert!(actions.is_action_pressed(&input, "Fire"));
}
#[test]
fn test_action_map_just_released() {
let mut input = Input::new();
let mut actions = ActionMap::new();
actions.bind_key("Charge", 99);
input.on_key_pressed(99);
input.begin_frame();
input.on_key_released(99);
assert!(actions.is_action_just_released(&input, "Charge"));
assert!(!actions.is_action_pressed(&input, "Charge"));
}
#[test]
fn test_bind_action_backward_compat() {
let mut actions = ActionMap::new();
actions.bind_action("Jump", 42); assert!(matches!(
actions.bindings.get("Jump").unwrap()[0],
InputBinding::Key(42)
));
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FrameRecord {
pub dt: f32,
pub input: Input,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct PlaybackData {
pub frames: Vec<FrameRecord>,
}
impl PlaybackData {
pub fn save(&self, path: &str) -> Result<(), String> {
let string_data = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.map_err(|e| format!("Serilestirme hatasi: {}", e))?;
std::fs::write(path, string_data).map_err(|e| format!("Dosya yazma hatasi: {}", e))?;
Ok(())
}
pub fn load(path: &str) -> Result<Self, String> {
let string_data =
std::fs::read_to_string(path).map_err(|e| format!("Dosya okuma hatasi: {}", e))?;
ron::from_str(&string_data).map_err(|e| format!("Deserilestirme hatasi: {}", e))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FrameActions {
pub pressed: HashSet<String>,
pub just_pressed: HashSet<String>,
pub just_released: HashSet<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FighterInputBuffer {
pub frames: std::collections::VecDeque<FrameActions>,
pub max_frames: usize,
}
impl FighterInputBuffer {
pub fn new(max_frames: usize) -> Self {
Self {
frames: std::collections::VecDeque::with_capacity(max_frames),
max_frames,
}
}
pub fn update(&mut self, input: &Input, action_map: &ActionMap, actions_to_track: &[&str]) {
let mut frame = FrameActions {
pressed: HashSet::new(),
just_pressed: HashSet::new(),
just_released: HashSet::new(),
};
for &action in actions_to_track {
if action_map.is_action_pressed(input, action) {
frame.pressed.insert(action.to_string());
}
if action_map.is_action_just_pressed(input, action) {
frame.just_pressed.insert(action.to_string());
}
if action_map.is_action_just_released(input, action) {
frame.just_released.insert(action.to_string());
}
}
self.frames.push_front(frame);
if self.frames.len() > self.max_frames {
self.frames.pop_back();
}
}
pub fn check_combo_strict(&self, sequence: &[&str], max_gap: usize) -> bool {
if sequence.is_empty() || self.frames.is_empty() {
return false;
}
let mut seq_idx = sequence.len() as isize - 1;
let mut frames_since_last_match = 0;
for frame in &self.frames {
if frames_since_last_match > max_gap {
return false;
}
let required_action = sequence[seq_idx as usize];
if frame.just_pressed.contains(required_action) || frame.pressed.contains(required_action) {
seq_idx -= 1;
frames_since_last_match = 0;
if seq_idx < 0 {
return true;
}
} else {
frames_since_last_match += 1;
}
}
false
}
}
impl Default for FighterInputBuffer {
fn default() -> Self {
Self::new(60)
}
}
#[cfg(test)]
mod fighter_tests {
use super::*;
#[test]
fn test_fighter_input_buffer_combo() {
let mut buffer = FighterInputBuffer::new(60);
let mut input = Input::new();
let mut action_map = ActionMap::new();
let mut frame1 = FrameActions {
pressed: ["Down".to_string()].into_iter().collect(),
just_pressed: [].into_iter().collect(),
just_released: [].into_iter().collect(),
};
buffer.frames.push_front(frame1);
let mut frame2 = FrameActions {
pressed: ["Down".to_string(), "Right".to_string()].into_iter().collect(),
just_pressed: ["Right".to_string()].into_iter().collect(),
just_released: [].into_iter().collect(),
};
buffer.frames.push_front(frame2);
let mut frame3 = FrameActions {
pressed: ["Right".to_string()].into_iter().collect(),
just_pressed: [].into_iter().collect(),
just_released: ["Down".to_string()].into_iter().collect(),
};
buffer.frames.push_front(frame3);
let mut frame4 = FrameActions {
pressed: ["LightPunch".to_string()].into_iter().collect(),
just_pressed: ["LightPunch".to_string()].into_iter().collect(),
just_released: [].into_iter().collect(),
};
buffer.frames.push_front(frame4);
let combo = ["Down", "Right", "LightPunch"];
assert!(buffer.check_combo_strict(&combo, 5), "Kombo basariyla algilanmali");
let wrong_combo = ["LightPunch", "Right", "Down"];
assert!(!buffer.check_combo_strict(&wrong_combo, 5), "Yanlis kombo sirasi algilanmamali");
}
#[test]
fn test_fighter_input_buffer_max_gap() {
let mut buffer = FighterInputBuffer::new(60);
let mut frame_down = FrameActions {
pressed: ["Down".to_string()].into_iter().collect(),
just_pressed: ["Down".to_string()].into_iter().collect(),
just_released: [].into_iter().collect(),
};
buffer.frames.push_front(frame_down);
for _ in 0..10 {
let empty = FrameActions {
pressed: [].into_iter().collect(),
just_pressed: [].into_iter().collect(),
just_released: [].into_iter().collect(),
};
buffer.frames.push_front(empty);
}
let mut frame_punch = FrameActions {
pressed: ["LightPunch".to_string()].into_iter().collect(),
just_pressed: ["LightPunch".to_string()].into_iter().collect(),
just_released: [].into_iter().collect(),
};
buffer.frames.push_front(frame_punch);
let combo = ["Down", "LightPunch"];
assert!(!buffer.check_combo_strict(&combo, 5), "Cok yavas basildi, algilanmamali");
assert!(buffer.check_combo_strict(&combo, 15), "Max gap genis oldugu icin algilanmali");
}
}