use alloc::string::String;
use alloc::vec::Vec;
use crate::behavior::program::{CExpr, CNode, COp};
use crate::behavior::value::{Arith, Cmp, Val};
use crate::components::{PlayCue, StoryPlayback, Transform};
use crate::ecs::{Entity, asset_id::AssetId};
use crate::math::sqrt;
pub struct View<'a> {
pub dt: f32,
pub elapsed: f32,
pub vars: &'a [Val],
pub locals: &'a [Val],
pub bindings: &'a mut [Option<Val>],
pub queries: &'a [Vec<Entity>],
pub by_name: &'a dyn Fn(AssetId) -> Option<Entity>,
pub transforms: &'a dyn Fn(Entity) -> Option<Transform>,
pub alive: &'a dyn Fn(Entity) -> bool,
pub self_entity: Option<Entity>,
pub trace: &'a mut Option<Vec<u32>>,
}
#[derive(Debug, Clone)]
pub enum Effect {
SetVar {
slot: u16,
value: Val,
add: bool,
},
SetLocal {
slot: u16,
value: Val,
add: bool,
},
SetTransform {
entity: Entity,
transform: Transform,
},
Spawn(SpawnEffect),
Despawn(Entity),
Reparent {
child: Entity,
parent: Option<Entity>,
},
Visible(Entity, bool),
Sound(PlayCue),
Scene {
scene: AssetId,
transition: String,
},
Screen(AssetId),
Story(StoryPlayback),
Save,
}
#[derive(Debug, Clone)]
pub struct SpawnEffect {
pub template: AssetId,
pub transform: Transform,
pub lifetime: Option<f32>,
}
fn eval(expr: &CExpr, view: &View<'_>) -> Option<Val> {
match expr {
CExpr::Lit(v) => Some(*v),
CExpr::Var(slot) => view.vars.get(*slot as usize).copied(),
CExpr::Local(slot) => view.locals.get(*slot as usize).copied(),
CExpr::Bind(slot) => view.bindings.get(*slot as usize).copied().flatten(),
CExpr::Named(id) => (view.by_name)(*id).map(Val::Entity),
CExpr::SelfEntity => view.self_entity.map(Val::Entity),
CExpr::Dt => Some(Val::Float(view.dt)),
CExpr::Elapsed => Some(Val::Float(view.elapsed)),
CExpr::Position(e) => {
let entity = eval(e, view)?.as_entity()?;
Some(Val::Vec3((view.transforms)(entity)?.position))
}
CExpr::Alive(e) => {
let alive = eval(e, view)
.and_then(Val::as_entity)
.is_some_and(view.alive);
Some(Val::Bool(alive))
}
CExpr::Distance(a, b) => {
let a = (view.transforms)(eval(a, view)?.as_entity()?)?.position;
let b = (view.transforms)(eval(b, view)?.as_entity()?)?.position;
let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
Some(Val::Float(sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2])))
}
CExpr::First(slot) => view
.queries
.get(*slot as usize)?
.first()
.copied()
.map(Val::Entity),
CExpr::Count(slot) => Some(Val::Int(view.queries.get(*slot as usize)?.len() as i32)),
CExpr::Normalize(e) => {
let v = eval(e, view)?.as_vec3()?;
let len = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
Some(Val::Vec3(if len > f32::EPSILON {
[v[0] / len, v[1] / len, v[2] / len]
} else {
[0.0; 3]
}))
}
CExpr::Arith(op, a, b) => arith(*op, eval(a, view)?, eval(b, view)?),
CExpr::Compare(op, a, b) => compare(*op, eval(a, view)?, eval(b, view)?),
CExpr::Not(e) => Some(Val::Bool(!eval(e, view)?.as_bool()?)),
CExpr::All(items) => {
for item in items {
if !eval(item, view)?.as_bool()? {
return Some(Val::Bool(false));
}
}
Some(Val::Bool(true))
}
CExpr::Any(items) => {
for item in items {
if eval(item, view)?.as_bool()? {
return Some(Val::Bool(true));
}
}
Some(Val::Bool(false))
}
CExpr::Never => None,
}
}
fn arith(op: Arith, a: Val, b: Val) -> Option<Val> {
let scalar = |x: f32, y: f32| match op {
Arith::Add => x + y,
Arith::Sub => x - y,
Arith::Mul => x * y,
Arith::Div => {
if y.abs() > f32::EPSILON {
x / y
} else {
0.0
}
}
};
match (a, b) {
(Val::Int(x), Val::Int(y)) => Some(Val::Int(scalar(x as f32, y as f32) as i32)),
(Val::Vec3(x), Val::Vec3(y)) => Some(Val::Vec3([
scalar(x[0], y[0]),
scalar(x[1], y[1]),
scalar(x[2], y[2]),
])),
(Val::Vec3(v), other) => {
let s = other.as_f32()?;
Some(Val::Vec3([
scalar(v[0], s),
scalar(v[1], s),
scalar(v[2], s),
]))
}
(other, Val::Vec3(v)) => {
let s = other.as_f32()?;
Some(Val::Vec3([
scalar(s, v[0]),
scalar(s, v[1]),
scalar(s, v[2]),
]))
}
(x, y) => Some(Val::Float(scalar(x.as_f32()?, y.as_f32()?))),
}
}
fn compare(op: Cmp, a: Val, b: Val) -> Option<Val> {
let equal = match (a, b) {
(Val::Entity(x), Val::Entity(y)) => x == y,
(Val::Bool(x), Val::Bool(y)) => x == y,
(x, y) => x.as_f32()? == y.as_f32()?,
};
Some(Val::Bool(match op {
Cmp::Eq => equal,
Cmp::Ne => !equal,
Cmp::Lt => a.as_f32()? < b.as_f32()?,
Cmp::Le => a.as_f32()? <= b.as_f32()?,
Cmp::Gt => a.as_f32()? > b.as_f32()?,
Cmp::Ge => a.as_f32()? >= b.as_f32()?,
}))
}
pub fn exec(nodes: &[CNode], view: &mut View<'_>, out: &mut Vec<Effect>) {
for node in nodes {
exec_node(node, view, out);
}
}
fn exec_node(node: &CNode, view: &mut View<'_>, out: &mut Vec<Effect>) {
if let Some(t) = view.trace.as_mut() {
t.push(node.id);
}
match &node.op {
COp::If {
cond,
then,
otherwise,
} => {
let Some(Val::Bool(pass)) = eval(cond, view) else {
return;
};
exec(if pass { then } else { otherwise }, view, out);
}
COp::ForEach { query, bind, body } => {
let Some(entities) = view.queries.get(*query as usize) else {
return;
};
for entity in entities.clone() {
set_binding(view, *bind, Some(Val::Entity(entity)));
exec(body, view, out);
}
}
COp::Let { bind, value } => {
let value = eval(value, view);
set_binding(view, *bind, value);
}
COp::SetVar { slot, value, add } => {
let Some(value) = eval(value, view) else {
return;
};
out.push(Effect::SetVar {
slot: *slot,
value,
add: *add,
});
}
COp::SetLocal { slot, value, add } => {
let Some(value) = eval(value, view) else {
return;
};
out.push(Effect::SetLocal {
slot: *slot,
value,
add: *add,
});
}
COp::SetTransform {
entity,
position,
rotation_deg,
scale,
} => {
let Some(entity) = eval(entity, view).and_then(Val::as_entity) else {
return;
};
let Some(mut transform) = (view.transforms)(entity) else {
return;
};
let field = |expr: &Option<CExpr>, into: &mut [f32; 3]| {
if let Some(expr) = expr
&& let Some(v) = eval(expr, view).and_then(Val::as_vec3)
{
*into = v;
}
};
field(position, &mut transform.position);
field(rotation_deg, &mut transform.rotation_deg);
field(scale, &mut transform.scale);
out.push(Effect::SetTransform { entity, transform });
}
COp::Spawn {
template,
position,
rotation_deg,
scale,
lifetime,
bind,
} => {
out.push(Effect::Spawn(SpawnEffect {
template: *template,
transform: Transform {
position: *position,
rotation_deg: *rotation_deg,
scale: *scale,
},
lifetime: (*lifetime > 0.0).then_some(*lifetime),
}));
if let Some(bind) = bind {
set_binding(view, *bind, None);
}
}
COp::Despawn(target) => {
if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
out.push(Effect::Despawn(entity));
}
}
COp::Reparent { child, parent } => {
let Some(child) = eval(child, view).and_then(Val::as_entity) else {
return;
};
let parent = match parent {
Some(expr) => match eval(expr, view).and_then(Val::as_entity) {
Some(entity) => Some(entity),
None => return,
},
None => None,
};
out.push(Effect::Reparent { child, parent });
}
COp::Visible(target, visible) => {
if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
out.push(Effect::Visible(entity, *visible));
}
}
COp::Sound { clip, kind, volume } => out.push(Effect::Sound(PlayCue {
clip: *clip,
kind: *kind,
volume: *volume,
priority: 0,
})),
COp::Scene { scene, transition } => out.push(Effect::Scene {
scene: *scene,
transition: transition.clone(),
}),
COp::Screen(screen) => out.push(Effect::Screen(*screen)),
COp::Story(playback) => out.push(Effect::Story(*playback)),
COp::Save => out.push(Effect::Save),
COp::Never => {}
}
}
fn set_binding(view: &mut View<'_>, slot: u16, value: Option<Val>) {
if let Some(slot) = view.bindings.get_mut(slot as usize) {
*slot = value;
}
}