nyanko 0.4.0

Pure stateless library for handling game quirks, animations, and data from The Battle Cats
Documentation
use crate::graphics::data::mamodel::{Model, ModelPart};
use crate::graphics::data::maanim::{Animation, AnimModification};

#[derive(Debug)]
pub enum TimelineError {
    BufferMismatch,
}

pub fn animate(model: &Model, animation: &Animation, global_frame: f32, state_buffer: &mut [ModelPart]) -> Result<(), TimelineError> {
    if state_buffer.len() != model.parts.len() {
        return Err(TimelineError::BufferMismatch);
    }

    state_buffer.clone_from_slice(&model.parts);

    for curve in &animation.curves {
        if curve.part_id >= state_buffer.len() { continue; }
        if curve.keyframes.is_empty() { continue; }

        let keyframe_min = curve.keyframes.first().map(|k| k.frame as f32).unwrap_or(0.0);
        let keyframe_max = curve.keyframes.last().map(|k| k.frame as f32).unwrap_or(0.0);

        let duration = (keyframe_max - keyframe_min).max(1.0);
        let mut local_frame = global_frame;

        if curve.loop_count != 1 {
            local_frame = (global_frame - keyframe_min).rem_euclid(duration) + keyframe_min;
        }

        let is_discrete = matches!(curve.modification_type, 0 | 1 | 3 | 13 | 14);

        let Some(interpolated_value) = interpolate_curve(curve, local_frame, is_discrete) else {
            continue;
        };

        let base_part = &model.parts[curve.part_id];
        let part = &mut state_buffer[curve.part_id];

        match curve.modification_type {
            0 => part.parent_id = interpolated_value as i32,
            1 => part.unit_id = interpolated_value as i32,
            2 => part.sprite_index = interpolated_value as i32,
            3 => part.drawing_layer = interpolated_value as i32,
            4 => part.position_x = (base_part.position_x + interpolated_value).trunc(),
            5 => part.position_y = (base_part.position_y + interpolated_value).trunc(),
            6 => part.pivot_x = (base_part.pivot_x + interpolated_value).trunc(),
            7 => part.pivot_y = (base_part.pivot_y + interpolated_value).trunc(),
            8 => {
                part.scale_x = (base_part.scale_x * interpolated_value / model.scale_unit).trunc();
                part.scale_y = (base_part.scale_y * interpolated_value / model.scale_unit).trunc();
            },
            9 => part.scale_x = (base_part.scale_x * interpolated_value / model.scale_unit).trunc(),
            10 => part.scale_y = (base_part.scale_y * interpolated_value / model.scale_unit).trunc(),
            11 => part.rotation = (base_part.rotation + interpolated_value).trunc(),
            12 => part.alpha = (base_part.alpha * interpolated_value / model.alpha_unit).trunc(),
            13 => part.flip_x = interpolated_value != 0.0,
            14 => part.flip_y = interpolated_value != 0.0,
            _ => {}
        }
    }

    Ok(())
}

fn interpolate_curve(curve: &AnimModification, frame: f32, is_discrete: bool) -> Option<f32> {
    if curve.keyframes.is_empty() { return None; }

    let first_keyframe = &curve.keyframes[0];
    if frame < first_keyframe.frame as f32 {
        return None;
    }

    let mut start_index = 0;
    let mut end_index = 0;
    let mut is_found = false;

    for (index, keyframe) in curve.keyframes.iter().enumerate() {
        if (keyframe.frame as f32) > frame {
            end_index = index;
            start_index = if index > 0 { index - 1 } else { 0 };
            is_found = true;
            break;
        }
    }

    if !is_found {
        let Some(last_keyframe) = curve.keyframes.last() else { return None; };
        return Some((last_keyframe.value as f32).trunc());
    }

    if end_index == 0 {
        return Some((curve.keyframes[0].value as f32).trunc());
    }

    let start_keyframe = &curve.keyframes[start_index];
    let end_keyframe = &curve.keyframes[end_index];

    if is_discrete { return Some((start_keyframe.value as f32).trunc()); }
    if start_keyframe.frame == end_keyframe.frame { return Some((start_keyframe.value as f32).trunc()); }

    if start_keyframe.ease_mode == 3 {
        let mut points = Vec::new();
        let mut backward_index = start_index as isize;

        while backward_index >= 0 {
            let current_keyframe = &curve.keyframes[backward_index as usize];
            if (backward_index as usize) != start_index && current_keyframe.ease_mode != 3 { break; }
            points.push((current_keyframe.frame as i64, current_keyframe.value as i64));
            backward_index -= 1;
        }

        points.reverse();
        let mut forward_index = end_index;

        while forward_index < curve.keyframes.len() {
            let current_keyframe = &curve.keyframes[forward_index];
            points.push((current_keyframe.frame as i64, current_keyframe.value as i64));
            if current_keyframe.ease_mode != 3 { break; }
            forward_index += 1;
        }

        let mut final_result: i64 = 0;
        let total_points = points.len();
        let frame_int = frame.trunc() as i64;

        for outer_index in 0..total_points {
            let (frame_j, val_j) = points[outer_index];
            let mut lagrange_term = val_j << 12;

            for inner_index in 0..total_points {
                if outer_index == inner_index { continue; }
                let (frame_m, _) = points[inner_index];
                if frame_j - frame_m != 0 {
                    lagrange_term = lagrange_term * (frame_int - frame_m) / (frame_j - frame_m);
                }
            }
            final_result += lagrange_term;
        }

        return Some((final_result / 4096) as f32);
    }

    let time_duration = (end_keyframe.frame - start_keyframe.frame) as f32;
    let time_current = frame.trunc() - (start_keyframe.frame as f32);
    let progress = time_current / time_duration;
    let start_value = start_keyframe.value as f32;
    let value_change = (end_keyframe.value - start_keyframe.value) as f32;

    let interpolated_value = match start_keyframe.ease_mode {
        0 => start_value + (value_change * progress).trunc(),
        1 => if progress >= 1.0 { end_keyframe.value as f32 } else { start_value },
        2 => {
            let ease_power = if start_keyframe.ease_power != 0 { start_keyframe.ease_power as f32 } else { 1.0 };
            let progress_clamped = progress.clamp(0.0, 1.0);
            let ease_factor = if ease_power >= 0.0 {
                1.0 - (1.0 - progress_clamped.powf(ease_power)).sqrt()
            } else {
                (1.0 - (1.0 - progress_clamped).powf(-ease_power)).sqrt()
            };

            if ease_factor.is_nan() {
                start_value + (value_change * progress).trunc()
            } else {
                start_value + (value_change * ease_factor).trunc()
            }
        },
        _ => start_value + (value_change * progress).trunc()
    };

    if curve.modification_type == 2 {
        if value_change < 0.0 {
            return Some(interpolated_value.ceil());
        } else {
            return Some(interpolated_value.floor());
        }
    }

    Some(interpolated_value.trunc())
}