use bevy::prelude::*;
use mortar_compiler::{Constant, Enum, IfCondition, Variable};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum MortarVariableValue {
String(String),
Number(f64),
Boolean(bool),
}
impl MortarVariableValue {
pub fn from_json(value: &serde_json::Value) -> Option<Self> {
match value {
serde_json::Value::String(s) => Some(MortarVariableValue::String(s.clone())),
serde_json::Value::Number(n) => n.as_f64().map(MortarVariableValue::Number),
serde_json::Value::Bool(b) => Some(MortarVariableValue::Boolean(*b)),
_ => None,
}
}
pub fn to_display_string(&self) -> String {
match self {
MortarVariableValue::String(s) => s.clone(),
MortarVariableValue::Number(n) => n.to_string(),
MortarVariableValue::Boolean(b) => b.to_string(),
}
}
}
#[derive(Debug, Clone)]
struct BranchDef {
enum_type: Option<String>,
cases: Vec<BranchCase>,
}
#[derive(Debug, Clone)]
struct BranchCase {
condition: String,
text: String,
}
#[derive(Component, Debug, Clone)]
pub struct MortarVariableState {
variables: HashMap<String, MortarVariableValue>,
branches: HashMap<String, BranchDef>,
}
impl Default for MortarVariableState {
fn default() -> Self {
Self::new()
}
}
fn parse_branch_variable(var: &Variable) -> Option<(String, BranchDef)> {
let obj = var.value.as_ref()?.as_object()?;
let enum_type = obj
.get("enum_type")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut cases = Vec::new();
if let Some(cases_array) = obj.get("cases").and_then(|v| v.as_array()) {
for case in cases_array {
if let Some(case_obj) = case.as_object() {
let condition = case_obj
.get("condition")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let text = case_obj
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
cases.push(BranchCase { condition, text });
}
}
}
Some((var.name.clone(), BranchDef { enum_type, cases }))
}
fn default_variable_value(var_type: &str, enums: &[Enum]) -> Option<MortarVariableValue> {
match var_type {
"String" => Some(MortarVariableValue::String(String::new())),
"Number" => Some(MortarVariableValue::Number(0.0)),
"Boolean" | "Bool" => Some(MortarVariableValue::Boolean(false)),
enum_type_name => {
let enum_def = enums.iter().find(|e| e.name == enum_type_name)?;
let first_member = enum_def.variants.first()?;
Some(MortarVariableValue::String(format!(
"{}.{}",
enum_def.name, first_member
)))
}
}
}
fn find_matching_case<'a>(
state: &MortarVariableState,
enum_type: Option<&str>,
cases: &'a [serde_json::Value],
) -> Option<&'a serde_json::Value> {
if let Some(enum_var_name) = enum_type {
let enum_value = state.get(enum_var_name)?;
let enum_member = enum_value.to_display_string();
let member_name = enum_member
.rfind('.')
.map_or(&*enum_member, |pos| &enum_member[pos + 1..]);
cases.iter().find(|case| {
case.get("condition")
.and_then(|c| c.as_str())
.map(|c| c == member_name)
.unwrap_or(false)
})
} else {
cases.iter().find(|case| {
case.get("condition")
.and_then(|c| c.as_str())
.and_then(|cond_name| state.get(cond_name))
.map(|v| matches!(v, MortarVariableValue::Boolean(true)))
.unwrap_or(false)
})
}
}
fn resolve_branch_text(state: &MortarVariableState, branch: &BranchDef) -> Option<String> {
let Some(enum_var_name) = &branch.enum_type else {
for case in &branch.cases {
if let Some(MortarVariableValue::Boolean(true)) = state.get(&case.condition) {
return Some(case.text.clone());
}
}
return None;
};
let enum_value = state.get(enum_var_name)?;
let enum_member = enum_value.to_display_string();
let member_name = enum_member
.rfind('.')
.map_or(&*enum_member, |pos| &enum_member[pos + 1..]);
branch
.cases
.iter()
.find(|case| case.condition == member_name)
.map(|case| case.text.clone())
}
impl MortarVariableState {
pub fn new() -> Self {
Self {
variables: HashMap::new(),
branches: HashMap::new(),
}
}
pub fn from_variables(variables: &[Variable], constants: &[Constant], enums: &[Enum]) -> Self {
let mut state = Self::new();
for constant in constants {
if let Some(parsed_value) = MortarVariableValue::from_json(&constant.value) {
state.set(&constant.name, parsed_value);
} else {
warn!("Failed to parse value for constant: {}", constant.name);
}
}
for var in variables {
if var.var_type == "Branch" {
state.branches.extend(parse_branch_variable(var));
continue;
}
if let Some(parsed) = var.value.as_ref().and_then(MortarVariableValue::from_json) {
state.set(&var.name, parsed);
continue;
}
if var.value.is_some() {
continue;
}
let Some(default_value) = default_variable_value(&var.var_type, enums) else {
continue;
};
state.set(&var.name, default_value);
}
state
}
pub fn set(&mut self, name: &str, value: MortarVariableValue) {
self.variables.insert(name.to_string(), value);
}
pub fn get(&self, name: &str) -> Option<&MortarVariableValue> {
self.variables.get(name)
}
pub fn execute_assignment(&mut self, var_name: &str, value_str: &str) {
if value_str.contains('.') {
self.set(var_name, MortarVariableValue::String(value_str.to_string()));
} else if value_str == "true" {
self.set(var_name, MortarVariableValue::Boolean(true));
} else if value_str == "false" {
self.set(var_name, MortarVariableValue::Boolean(false));
} else if let Ok(num) = value_str.parse::<f64>() {
self.set(var_name, MortarVariableValue::Number(num));
} else {
self.set(var_name, MortarVariableValue::String(value_str.to_string()));
}
}
pub fn set_branch_text(&mut self, name: String, text: String) {
let branch_def = BranchDef {
enum_type: None,
cases: vec![BranchCase {
condition: "default".to_string(),
text: text.clone(),
}],
};
self.branches.insert(name.clone(), branch_def);
self.set("default", MortarVariableValue::Boolean(true));
}
pub fn get_branch_events(
&self,
name: &str,
variables: &[Variable],
) -> Option<Vec<mortar_compiler::Event>> {
let branch_var = variables
.iter()
.find(|v| v.name == name && v.var_type == "Branch")?;
let value = branch_var.value.as_ref()?;
let cases = value.get("cases")?.as_array()?;
let enum_type = value.get("enum_type").and_then(|v| v.as_str());
let matching_case = find_matching_case(self, enum_type, cases)?;
let events_array = matching_case.get("events")?.as_array()?;
let mut result = Vec::new();
for event_json in events_array {
if let Ok(event) = serde_json::from_value::<mortar_compiler::Event>(event_json.clone())
{
result.push(event);
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
pub fn get_branch_text(&self, name: &str) -> Option<String> {
let branch = self.branches.get(name)?;
resolve_branch_text(self, branch)
}
pub fn evaluate_condition(&self, condition: &IfCondition) -> bool {
match condition.cond_type.as_str() {
"binary" => self.evaluate_binary_condition(condition),
"unary" => self.evaluate_unary_condition(condition),
"identifier" => self.evaluate_identifier_condition(condition),
"literal" => self.evaluate_literal_condition(condition),
"func_call" => self.evaluate_func_call_condition(condition),
_ => {
warn!("Unknown condition type: {}", condition.cond_type);
false
}
}
}
fn evaluate_binary_condition(&self, condition: &IfCondition) -> bool {
let left = condition.left.as_ref().unwrap();
let right = condition.right.as_ref().unwrap();
let operator = condition.operator.as_ref().unwrap();
match operator.as_str() {
"&&" | "||" => {
let left_value = self.evaluate_condition(left);
let right_value = self.evaluate_condition(right);
match operator.as_str() {
"&&" => left_value && right_value,
"||" => left_value || right_value,
_ => unreachable!(),
}
}
">" => self.compare_values(left, right, |a, b| a > b),
"<" => self.compare_values(left, right, |a, b| a < b),
">=" => self.compare_values(left, right, |a, b| a >= b),
"<=" => self.compare_values(left, right, |a, b| a <= b),
"==" => self.compare_values_eq(left, right, true),
"!=" => self.compare_values_eq(left, right, false),
_ => {
warn!("Unknown binary operator: {}", operator);
false
}
}
}
fn evaluate_unary_condition(&self, condition: &IfCondition) -> bool {
let operand = condition.operand.as_ref().unwrap();
let operator = condition.operator.as_ref().unwrap();
match operator.as_str() {
"!" => !self.evaluate_condition(operand),
_ => {
warn!("Unknown unary operator: {}", operator);
false
}
}
}
fn evaluate_identifier_condition(&self, condition: &IfCondition) -> bool {
let identifier = condition.value.as_ref().unwrap();
match self.get(identifier) {
Some(MortarVariableValue::Boolean(b)) => *b,
Some(_) => {
warn!(
"Variable '{}' is not a boolean, cannot evaluate as condition",
identifier
);
false
}
None => {
warn!("Variable '{}' not found", identifier);
false
}
}
}
fn evaluate_literal_condition(&self, condition: &IfCondition) -> bool {
let value = condition.value.as_ref().unwrap();
match value.as_str() {
"true" => true,
"false" => false,
_ => {
warn!("Unknown literal value: {}", value);
false
}
}
}
fn evaluate_func_call_condition(&self, _condition: &IfCondition) -> bool {
dev_info!("Function call in condition requires runtime function evaluation");
false
}
fn compare_values<F>(&self, left: &IfCondition, right: &IfCondition, cmp: F) -> bool
where
F: Fn(f64, f64) -> bool,
{
let left_num = self.get_numeric_value(left);
let right_num = self.get_numeric_value(right);
match (left_num, right_num) {
(Some(l), Some(r)) => cmp(l, r),
_ => {
warn!("Cannot compare non-numeric values");
false
}
}
}
fn compare_values_eq(
&self,
left: &IfCondition,
right: &IfCondition,
expect_equal: bool,
) -> bool {
let left_num = self.get_numeric_value(left);
let right_num = self.get_numeric_value(right);
if let (Some(l), Some(r)) = (left_num, right_num) {
let is_equal = (l - r).abs() < f64::EPSILON;
return if expect_equal { is_equal } else { !is_equal };
}
let left_str = self.get_string_value(left);
let right_str = self.get_string_value(right);
if let (Some(l), Some(r)) = (left_str, right_str) {
let is_equal = l == r;
return if expect_equal { is_equal } else { !is_equal };
}
false
}
fn get_string_value(&self, condition: &IfCondition) -> Option<String> {
match condition.cond_type.as_str() {
"identifier" => {
let identifier = condition.value.as_ref()?;
self.get(identifier).map(|val| val.to_display_string())
}
"enum_member" => condition.value.clone(),
"literal" => condition.value.clone(),
_ => None,
}
}
fn get_numeric_value(&self, condition: &IfCondition) -> Option<f64> {
match condition.cond_type.as_str() {
"identifier" => {
let identifier = condition.value.as_ref()?;
if let Ok(num) = identifier.parse::<f64>() {
return Some(num);
}
match self.get(identifier) {
Some(MortarVariableValue::Number(n)) => Some(*n),
_ => None,
}
}
"literal" => {
let value = condition.value.as_ref()?;
value.parse::<f64>().ok()
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_variable_state_basic() {
let mut state = MortarVariableState::new();
state.set("score", MortarVariableValue::Number(100.0));
state.set("name", MortarVariableValue::String("Player".to_string()));
state.set("is_active", MortarVariableValue::Boolean(true));
assert_eq!(
state.get("score"),
Some(&MortarVariableValue::Number(100.0))
);
assert_eq!(
state.get("name"),
Some(&MortarVariableValue::String("Player".to_string()))
);
assert_eq!(
state.get("is_active"),
Some(&MortarVariableValue::Boolean(true))
);
}
#[test]
fn test_evaluate_simple_condition() {
let mut state = MortarVariableState::new();
state.set("is_winner", MortarVariableValue::Boolean(true));
let condition = IfCondition {
cond_type: "identifier".to_string(),
operator: None,
left: None,
right: None,
operand: None,
value: Some("is_winner".to_string()),
};
assert!(state.evaluate_condition(&condition));
}
#[test]
fn test_evaluate_comparison() {
let mut state = MortarVariableState::new();
state.set("score", MortarVariableValue::Number(150.0));
let condition = IfCondition {
cond_type: "binary".to_string(),
operator: Some(">".to_string()),
left: Some(Box::new(IfCondition {
cond_type: "identifier".to_string(),
operator: None,
left: None,
right: None,
operand: None,
value: Some("score".to_string()),
})),
right: Some(Box::new(IfCondition {
cond_type: "literal".to_string(),
operator: None,
left: None,
right: None,
operand: None,
value: Some("100".to_string()),
})),
operand: None,
value: None,
};
assert!(state.evaluate_condition(&condition));
}
}