use concinnity_core::components::{
Behavior, BehaviorExpr, BehaviorNode, BehaviorSource, Prop, PropInstance,
};
use concinnity_core::components::{BehaviorQuery, Camera3D, Transform, Variables};
use concinnity_core::ecs::Ref;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::ecs::{MeshHandle, World};
use crate::ecs::SYSTEMS;
#[test]
fn a_prop_scoped_behavior_fires_once_started() {
let mut world = World::new();
world.push_identified(
AssetId(1),
Prop {
mesh: Some(MeshHandle(10)),
scale: [1.0; 3],
..Default::default()
},
);
world.push_identified(
AssetId(2),
Prop {
model: Some(Ref::new(AssetId(20))),
position: [10.0, 0.0, 0.0],
scale: [1.0; 3],
..Default::default()
},
);
world.add_component(Behavior {
on: BehaviorSource::Tick,
scope: vec!["Prop".into()],
body: vec![BehaviorNode::SetTransform {
entity: BehaviorExpr::SelfEntity,
position: Some(BehaviorExpr::Add(
Box::new(BehaviorExpr::Position(Box::new(BehaviorExpr::SelfEntity))),
Box::new(BehaviorExpr::Vec3([0.0, 1.0, 0.0])),
)),
rotation_deg: None,
scale: None,
}],
..Default::default()
});
world.start(SYSTEMS).unwrap();
assert_eq!(
world.query::<Prop>().count(),
0,
"decomposition drained the authored column",
);
world.step();
let mut lifted: Vec<f32> = world
.join2::<PropInstance, Transform>()
.map(|(_, _, t)| t.position[1])
.collect();
lifted.sort_by(f32::total_cmp);
assert_eq!(
lifted,
vec![1.0, 1.0],
"both the mesh- and the model-backed prop ran the behavior",
);
}
#[test]
fn a_behavior_moves_a_prop_the_simulation_owns() {
use concinnity_core::components::{PropBody, PropCollider};
let mut world = World::new();
world.push_identified(
AssetId(1),
Prop {
mesh: Some(MeshHandle(10)),
position: [0.0, 5.0, 0.0],
scale: [1.0; 3],
collider: Some(PropCollider {
shape: concinnity_core::components::PropColliderShape::Ball,
radius: 0.5,
half_extents: [0.5; 3],
..Default::default()
}),
..Default::default()
},
);
world.add_component(PropBody {
prop_name: Some(Ref::new(AssetId(1))),
mass: 1.0,
..Default::default()
});
world.add_component(Behavior {
on: BehaviorSource::Tick,
scope: vec!["Prop".into()],
body: vec![BehaviorNode::SetTransform {
entity: BehaviorExpr::SelfEntity,
position: Some(BehaviorExpr::Add(
Box::new(BehaviorExpr::Position(Box::new(BehaviorExpr::SelfEntity))),
Box::new(BehaviorExpr::Vec3([0.0, 4.0, 0.0])),
)),
rotation_deg: None,
scale: None,
}],
..Default::default()
});
world.start(SYSTEMS).unwrap();
world.step();
let y = world
.join2::<PropInstance, Transform>()
.map(|(_, _, t)| t.position[1])
.next()
.expect("the prop kept its transform");
assert!(
(y - 9.0).abs() < 0.05,
"the body was moved to where the behavior put it (y = {y})",
);
}
#[test]
fn a_saving_world_restores_its_variable_through_the_file_store() {
use super::save::FileStore;
use concinnity_core::behavior::{BehaviorStore, BehaviorSystem};
use concinnity_core::components::{BehaviorLiteral, VariableDecl};
use concinnity_core::ecs::System;
let tree = concinnity_testing::TempTree::new();
let dir = tree.join("state");
let saver = || Behavior {
on: BehaviorSource::Start,
body: vec![
BehaviorNode::Set {
var: "visits".into(),
value: BehaviorExpr::Int(1),
add: true,
},
BehaviorNode::Save,
],
..Default::default()
};
let declared = || Variables {
vars: vec![VariableDecl {
name: "visits".into(),
value: BehaviorLiteral::Int(0),
}],
};
let run = |expected: i32, what: &str| {
let mut world = World::new();
world.add_component(saver());
world.add_component(declared());
let mut system = BehaviorSystem::new().with_store(Box::new(FileStore::at(&dir)));
system.init(&mut world.context());
system.step(&mut world.context());
let state = FileStore::at(&dir)
.read()
.expect("the save node wrote through the store");
assert_eq!(
state.vars.get("visits"),
Some(&BehaviorLiteral::Int(expected)),
"{what}"
);
};
run(1, "the tick's value reached the file");
run(2, "the second run started from what the first stored");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_distance_gate_on_the_queried_camera_decides_by_where_the_camera_is() {
use concinnity_core::components::cook::Camera3D as Camera3DArgs;
let mut world = World::new();
world.push_identified(
AssetId(1),
Prop {
mesh: Some(MeshHandle(10)),
scale: [1.0; 3],
..Default::default()
},
);
world.add_component(Camera3D::bake(Camera3DArgs {
position: [0.0, 0.0, 100.0],
controller: None,
..Default::default()
}));
world.add_component(Behavior {
on: BehaviorSource::Tick,
scope: vec!["Prop".into()],
queries: vec![BehaviorQuery {
name: "player".into(),
has: vec!["Camera3D".into()],
}],
body: vec![
BehaviorNode::Let {
name: "target".into(),
value: BehaviorExpr::First("player".into()),
},
BehaviorNode::If {
cond: BehaviorExpr::Lt(
Box::new(BehaviorExpr::Distance(
Box::new(BehaviorExpr::SelfEntity),
Box::new(BehaviorExpr::Bind("target".into())),
)),
Box::new(BehaviorExpr::Float(20.0)),
),
then: vec![BehaviorNode::SetTransform {
entity: BehaviorExpr::SelfEntity,
position: Some(BehaviorExpr::Add(
Box::new(BehaviorExpr::Position(Box::new(BehaviorExpr::SelfEntity))),
Box::new(BehaviorExpr::Mul(
Box::new(BehaviorExpr::Normalize(Box::new(BehaviorExpr::Sub(
Box::new(BehaviorExpr::Position(Box::new(BehaviorExpr::Bind(
"target".into(),
)))),
Box::new(BehaviorExpr::Position(Box::new(
BehaviorExpr::SelfEntity,
))),
)))),
Box::new(BehaviorExpr::Float(2.0)),
)),
)),
rotation_deg: None,
scale: None,
}],
otherwise: Vec::new(),
},
],
..Default::default()
});
world.start(SYSTEMS).unwrap();
let chased = |world: &mut World| {
world
.join2::<PropInstance, Transform>()
.map(|(_, _, t)| t.position[2])
.next()
.expect("the prop kept its transform")
};
world.step();
assert_eq!(chased(&mut world), 0.0, "the far camera left the gate shut");
for camera in world.context().query_mut::<Camera3D>() {
camera.position = [0.0, 0.0, 5.0];
}
world.step();
assert_eq!(
chased(&mut world),
2.0,
"the near camera opened the gate and the prop stepped toward it",
);
}