souprune 0.5.1

A game framework designed specifically for Deltarune / Undertale fangames.
Documentation
use super::super::components::IndexBound;
use super::super::layout::FloatOrExpr;
use crate::app_state::overworld::OverworldState;
use crate::core::input::Action;
use bevy::prelude::*;

pub fn parse_action(action_str: &str) -> Option<Action> {
    match action_str {
        "Up" => Some(Action::Up),
        "Down" => Some(Action::Down),
        "Left" => Some(Action::Left),
        "Right" => Some(Action::Right),
        "Confirm" => Some(Action::Confirm),
        "Cancel" => Some(Action::Cancel),
        "Menu" => Some(Action::Menu),
        _ => None,
    }
}

pub fn parse_overworld_state(state_str: &str) -> Option<OverworldState> {
    match state_str {
        "Normal" => Some(OverworldState::Normal),
        "Backpack" => Some(OverworldState::Backpack),
        "Cutscene" => Some(OverworldState::Cutscene),
        _ => None,
    }
}

pub fn evaluate_index_bound(
    bound: &IndexBound,
    player_data: &crate::core::data::PlayerData,
) -> usize {
    match bound {
        IndexBound::Static(value) => *value,
        IndexBound::Dynamic(expr) => evaluate_index_expression(expr, player_data),
    }
}

fn evaluate_index_expression(expr: &str, player_data: &crate::core::data::PlayerData) -> usize {
    let expr = expr.trim();

    if expr == "inventory.len()" {
        return player_data.inventory.len();
    }

    if expr == "inventory_capacity" {
        return player_data.inventory_capacity;
    }

    if expr.starts_with("min(") && expr.ends_with(")") {
        let inner = &expr[4..expr.len() - 1];
        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
        if parts.len() == 2 {
            let a = evaluate_index_expression(parts[0], player_data);
            let b = evaluate_index_expression(parts[1], player_data);
            return a.min(b);
        }
    }

    if expr.starts_with("max(") && expr.ends_with(")") {
        let inner = &expr[4..expr.len() - 1];
        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
        if parts.len() == 2 {
            let a = evaluate_index_expression(parts[0], player_data);
            let b = evaluate_index_expression(parts[1], player_data);
            return a.max(b);
        }
    }

    if let Ok(value) = expr.parse::<usize>() {
        return value;
    }

    warn!(
        "Unable to evaluate index expression: {}, defaulting to 1",
        expr
    );
    1
}

pub fn evaluate_float_expr(
    expr: &FloatOrExpr,
    player_data: &crate::core::data::PlayerData,
    time: Option<f64>,
) -> f32 {
    match expr {
        FloatOrExpr::Static(v) => *v,
        FloatOrExpr::Dynamic(expr_str) => {
            use evalexpr::{
                ContextWithMutableFunctions, ContextWithMutableVariables, DefaultNumericTypes,
                HashMapContext,
            };

            let mut context: HashMapContext<DefaultNumericTypes> = HashMapContext::new();

            // Register sin function
            let _ = context.set_function(
                "sin".to_string(),
                evalexpr::Function::new(|arg| {
                    let val: f64 = arg.as_float()?;
                    Ok(evalexpr::Value::Float(val.sin()))
                }),
            );

            // Register cos function
            let _ = context.set_function(
                "cos".to_string(),
                evalexpr::Function::new(|arg| {
                    let val: f64 = arg.as_float()?;
                    Ok(evalexpr::Value::Float(val.cos()))
                }),
            );

            // Register snap function (snap(val, step))
            let _ = context.set_function(
                "snap".to_string(),
                evalexpr::Function::new(|arg| {
                    if let Ok(tuple) = arg.as_tuple() {
                        if tuple.len() != 2 {
                            return Err(evalexpr::EvalexprError::CustomMessage(
                                "snap expects 2 arguments".to_string(),
                            ));
                        }
                        let val: f64 = tuple[0].as_float()?;
                        let step: f64 = tuple[1].as_float()?;
                        if step == 0.0 {
                            return Ok(evalexpr::Value::Float(val));
                        }
                        // Use floor to snap to the previous step (like 30fps update)
                        let snapped: f64 = (val / step).floor() * step;
                        Ok(evalexpr::Value::Float(snapped))
                    } else {
                        // If single argument, maybe return as is or error?
                        // But snap needs step.
                        Err(evalexpr::EvalexprError::CustomMessage(
                            "snap expects 2 arguments (val, step)".to_string(),
                        ))
                    }
                }),
            );

            let _ = context.set_value(
                "@player.hp".to_string(),
                evalexpr::Value::Int(player_data.hp as i64),
            );
            let _ = context.set_value(
                "@player.hp_max".to_string(),
                evalexpr::Value::Int(player_data.hp_max as i64),
            );
            let _ = context.set_value(
                "@player.lv".to_string(),
                evalexpr::Value::Int(player_data.lv as i64),
            );

            if let Some(t) = time {
                // debug!("Setting @time to {}", t);
                let _ = context.set_value("@time".to_string(), evalexpr::Value::Float(t));
            } else {
                let _ = context.set_value("@time".to_string(), evalexpr::Value::Float(0.0));
            }

            match evalexpr::eval_with_context(expr_str, &context) {
                Ok(val) => {
                    if let Ok(f) = val.as_float() {
                        let f_f64: f64 = f;
                        f_f64 as f32
                    } else if let Ok(i) = val.as_int() {
                        let i_i64: i64 = i;
                        i_i64 as f32
                    } else {
                        warn!(
                            "Failed to convert expression result to number: {}",
                            expr_str
                        );
                        0.0
                    }
                }
                Err(e) => {
                    warn!("Failed to evaluate expression '{}': {}", expr_str, e);
                    0.0
                }
            }
        }
    }
}

pub fn resolve_text_content(
    template: &str,
    mortar_strings: &crate::extra::mortar::MortarStringTable,
    player_data: &crate::core::data::PlayerData,
    item_registry: &crate::core::item::ItemRegistry,
) -> String {
    let mut result = String::new();
    let mut chars = template.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '{'
            && let Some(&next_ch) = chars.peek()
        {
            if next_ch == '{' {
                chars.next();
                let mut key = String::new();
                let mut found_closing = false;

                while let Some(ch) = chars.next() {
                    if ch == '}'
                        && let Some(&next_ch) = chars.peek()
                        && next_ch == '}'
                    {
                        chars.next();
                        found_closing = true;
                        break;
                    }
                    key.push(ch);
                }

                if found_closing {
                    let resolved = mortar_strings.resolve(&key);
                    result.push_str(resolved);
                } else {
                    result.push_str("{{");
                    result.push_str(&key);
                }
                continue;
            } else if next_ch == '@' {
                chars.next();
                let mut path = String::new();
                let mut found_closing = false;

                for ch in chars.by_ref() {
                    if ch == '}' {
                        found_closing = true;
                        break;
                    }
                    path.push(ch);
                }

                if found_closing {
                    let value =
                        resolve_data_path(&path, player_data, item_registry, mortar_strings);
                    result.push_str(&value);
                } else {
                    result.push_str("{@");
                    result.push_str(&path);
                }
                continue;
            }
        }
        result.push(ch);
    }

    result
}

pub fn evaluate_condition(condition: &str, player_data: &crate::core::data::PlayerData) -> bool {
    match condition {
        "player.inventory.is_empty" => player_data.inventory.is_empty(),
        "player.inventory.is_not_empty" => !player_data.inventory.is_empty(),
        _ => {
            if condition.starts_with("player.") {
                let parts: Vec<&str> = condition.split('.').collect();
                if parts.len() >= 3 {
                    match (parts[1], parts[2]) {
                        ("hp", "is_low") => player_data.hp < player_data.hp_max / 4,
                        ("hp", "is_critical") => player_data.hp <= 1,
                        ("gold", "is_zero") => player_data.gold == 0,
                        _ => false,
                    }
                } else {
                    false
                }
            } else {
                false
            }
        }
    }
}

pub fn resolve_data_path(
    path: &str,
    player_data: &crate::core::data::PlayerData,
    item_registry: &crate::core::item::ItemRegistry,
    mortar_strings: &crate::extra::mortar::MortarStringTable,
) -> String {
    use crate::core::item::ItemType;

    match path {
        "player.name" => player_data.name.clone(),
        "player.lv" => player_data.lv.to_string(),
        "player.hp" => {
            let result = player_data.hp.to_string();
            info!("[resolve_data_path] player.hp = {}", result);
            result
        }
        "player.hp_max" => {
            let result = player_data.hp_max.to_string();
            info!("[resolve_data_path] player.hp_max = {}", result);
            result
        }
        "player.gold" => player_data.gold.to_string(),
        "player.exp" => player_data.exp.to_string(),
        "player.next_exp" => player_data.next_exp.to_string(),
        "player.attack" => player_data.attack.to_string(),
        "player.defense" => player_data.defense.to_string(),
        "player.inventory" => player_data
            .inventory
            .iter()
            .take(8)
            .map(|item_id| {
                if let Some(item) = item_registry.get(&item_id.0) {
                    let key = format!("{}:{}", item.locate_file, item.locate_name);
                    mortar_strings.resolve(&key).to_string()
                } else {
                    warn!("Item ID '{}' not found in registry!", item_id.0);
                    format!("UNDEFINED ({})", item_id.0)
                }
            })
            .collect::<Vec<String>>()
            .join("\n"),
        "player.weapon" => {
            if let Some(item) = item_registry.get(&player_data.weapon.0) {
                let key = format!("{}:{}", item.locate_file, item.locate_name);
                mortar_strings.resolve(&key).to_string()
            } else {
                player_data.weapon.0.clone()
            }
        }
        "player.weapon_atk" => {
            if let Some(item) = item_registry.get(&player_data.weapon.0)
                && let ItemType::Weapon { damage, .. } = item.item_type
            {
                return damage.to_string();
            }
            "0".to_string()
        }
        "player.total_attack" => {
            let weapon_atk = if let Some(item) = item_registry.get(&player_data.weapon.0) {
                if let ItemType::Weapon { damage, .. } = item.item_type {
                    damage as usize
                } else {
                    0
                }
            } else {
                0
            };
            (player_data.attack + weapon_atk).to_string()
        }
        "player.armor" => {
            if let Some(item) = item_registry.get(&player_data.armor.0) {
                let key = format!("{}:{}", item.locate_file, item.locate_name);
                mortar_strings.resolve(&key).to_string()
            } else {
                player_data.armor.0.clone()
            }
        }
        "player.armor_def" => {
            if let Some(item) = item_registry.get(&player_data.armor.0)
                && let ItemType::Armor { defense } = item.item_type
            {
                return defense.to_string();
            }
            "0".to_string()
        }
        "player.total_defense" => {
            let armor_def = if let Some(item) = item_registry.get(&player_data.armor.0) {
                if let ItemType::Armor { defense } = item.item_type {
                    defense as usize
                } else {
                    0
                }
            } else {
                0
            };
            (player_data.defense + armor_def).to_string()
        }
        _ => format!("<unknown:{}>", path),
    }
}