1use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::behavior::program::{CExpr, CNode, COp};
13use crate::behavior::value::{Arith, Cmp, Val};
14use crate::components::{PlayCue, StoryPlayback, Transform};
15use crate::ecs::{Entity, asset_id::AssetId};
16use crate::math::sqrt;
17
18pub struct View<'a> {
20 pub dt: f32,
22 pub elapsed: f32,
24 pub vars: &'a [Val],
26 pub locals: &'a [Val],
28 pub bindings: &'a mut [Option<Val>],
31 pub queries: &'a [Vec<Entity>],
33 pub by_name: &'a dyn Fn(AssetId) -> Option<Entity>,
35 pub transforms: &'a dyn Fn(Entity) -> Option<Transform>,
37 pub alive: &'a dyn Fn(Entity) -> bool,
39 pub self_entity: Option<Entity>,
41 pub trace: &'a mut Option<Vec<u32>>,
44}
45
46#[derive(Debug, Clone)]
48pub enum Effect {
49 SetVar {
51 slot: u16,
53 value: Val,
55 add: bool,
57 },
58 SetLocal {
60 slot: u16,
62 value: Val,
64 add: bool,
66 },
67 SetTransform {
69 entity: Entity,
71 transform: Transform,
73 },
74 Spawn(SpawnEffect),
76 Despawn(Entity),
78 Reparent {
80 child: Entity,
82 parent: Option<Entity>,
84 },
85 Visible(Entity, bool),
87 Sound(PlayCue),
89 Scene {
91 scene: AssetId,
93 transition: String,
95 },
96 Screen(AssetId),
98 Story(StoryPlayback),
100 Save,
102}
103
104#[derive(Debug, Clone)]
106pub struct SpawnEffect {
107 pub template: AssetId,
109 pub transform: Transform,
111 pub lifetime: Option<f32>,
113}
114
115fn eval(expr: &CExpr, view: &View<'_>) -> Option<Val> {
116 match expr {
117 CExpr::Lit(v) => Some(*v),
118 CExpr::Var(slot) => view.vars.get(*slot as usize).copied(),
119 CExpr::Local(slot) => view.locals.get(*slot as usize).copied(),
120 CExpr::Bind(slot) => view.bindings.get(*slot as usize).copied().flatten(),
121 CExpr::Named(id) => (view.by_name)(*id).map(Val::Entity),
122 CExpr::SelfEntity => view.self_entity.map(Val::Entity),
123 CExpr::Dt => Some(Val::Float(view.dt)),
124 CExpr::Elapsed => Some(Val::Float(view.elapsed)),
125 CExpr::Position(e) => {
126 let entity = eval(e, view)?.as_entity()?;
127 Some(Val::Vec3((view.transforms)(entity)?.position))
128 }
129 CExpr::Alive(e) => {
130 let alive = eval(e, view)
132 .and_then(Val::as_entity)
133 .is_some_and(view.alive);
134 Some(Val::Bool(alive))
135 }
136 CExpr::Distance(a, b) => {
137 let a = (view.transforms)(eval(a, view)?.as_entity()?)?.position;
138 let b = (view.transforms)(eval(b, view)?.as_entity()?)?.position;
139 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
140 Some(Val::Float(sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2])))
141 }
142 CExpr::First(slot) => view
143 .queries
144 .get(*slot as usize)?
145 .first()
146 .copied()
147 .map(Val::Entity),
148 CExpr::Count(slot) => Some(Val::Int(view.queries.get(*slot as usize)?.len() as i32)),
149 CExpr::Normalize(e) => {
150 let v = eval(e, view)?.as_vec3()?;
151 let len = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
152 Some(Val::Vec3(if len > f32::EPSILON {
153 [v[0] / len, v[1] / len, v[2] / len]
154 } else {
155 [0.0; 3]
156 }))
157 }
158 CExpr::Arith(op, a, b) => arith(*op, eval(a, view)?, eval(b, view)?),
159 CExpr::Compare(op, a, b) => compare(*op, eval(a, view)?, eval(b, view)?),
160 CExpr::Not(e) => Some(Val::Bool(!eval(e, view)?.as_bool()?)),
161 CExpr::All(items) => {
162 for item in items {
163 if !eval(item, view)?.as_bool()? {
164 return Some(Val::Bool(false));
165 }
166 }
167 Some(Val::Bool(true))
168 }
169 CExpr::Any(items) => {
170 for item in items {
171 if eval(item, view)?.as_bool()? {
172 return Some(Val::Bool(true));
173 }
174 }
175 Some(Val::Bool(false))
176 }
177 CExpr::Never => None,
178 }
179}
180
181fn arith(op: Arith, a: Val, b: Val) -> Option<Val> {
182 let scalar = |x: f32, y: f32| match op {
183 Arith::Add => x + y,
184 Arith::Sub => x - y,
185 Arith::Mul => x * y,
186 Arith::Div => {
189 if y.abs() > f32::EPSILON {
190 x / y
191 } else {
192 0.0
193 }
194 }
195 };
196 match (a, b) {
197 (Val::Int(x), Val::Int(y)) => Some(Val::Int(scalar(x as f32, y as f32) as i32)),
198 (Val::Vec3(x), Val::Vec3(y)) => Some(Val::Vec3([
199 scalar(x[0], y[0]),
200 scalar(x[1], y[1]),
201 scalar(x[2], y[2]),
202 ])),
203 (Val::Vec3(v), other) => {
204 let s = other.as_f32()?;
205 Some(Val::Vec3([
206 scalar(v[0], s),
207 scalar(v[1], s),
208 scalar(v[2], s),
209 ]))
210 }
211 (other, Val::Vec3(v)) => {
212 let s = other.as_f32()?;
213 Some(Val::Vec3([
214 scalar(s, v[0]),
215 scalar(s, v[1]),
216 scalar(s, v[2]),
217 ]))
218 }
219 (x, y) => Some(Val::Float(scalar(x.as_f32()?, y.as_f32()?))),
220 }
221}
222
223fn compare(op: Cmp, a: Val, b: Val) -> Option<Val> {
224 let equal = match (a, b) {
225 (Val::Entity(x), Val::Entity(y)) => x == y,
226 (Val::Bool(x), Val::Bool(y)) => x == y,
227 (x, y) => x.as_f32()? == y.as_f32()?,
228 };
229 Some(Val::Bool(match op {
230 Cmp::Eq => equal,
231 Cmp::Ne => !equal,
232 Cmp::Lt => a.as_f32()? < b.as_f32()?,
233 Cmp::Le => a.as_f32()? <= b.as_f32()?,
234 Cmp::Gt => a.as_f32()? > b.as_f32()?,
235 Cmp::Ge => a.as_f32()? >= b.as_f32()?,
236 }))
237}
238
239pub fn exec(nodes: &[CNode], view: &mut View<'_>, out: &mut Vec<Effect>) {
241 for node in nodes {
242 exec_node(node, view, out);
243 }
244}
245
246fn exec_node(node: &CNode, view: &mut View<'_>, out: &mut Vec<Effect>) {
247 if let Some(t) = view.trace.as_mut() {
248 t.push(node.id);
249 }
250 match &node.op {
251 COp::If {
252 cond,
253 then,
254 otherwise,
255 } => {
256 let Some(Val::Bool(pass)) = eval(cond, view) else {
257 return;
258 };
259 exec(if pass { then } else { otherwise }, view, out);
260 }
261 COp::ForEach { query, bind, body } => {
262 let Some(entities) = view.queries.get(*query as usize) else {
263 return;
264 };
265 for entity in entities.clone() {
268 set_binding(view, *bind, Some(Val::Entity(entity)));
269 exec(body, view, out);
270 }
271 }
272 COp::Let { bind, value } => {
273 let value = eval(value, view);
274 set_binding(view, *bind, value);
275 }
276 COp::SetVar { slot, value, add } => {
277 let Some(value) = eval(value, view) else {
278 return;
279 };
280 out.push(Effect::SetVar {
281 slot: *slot,
282 value,
283 add: *add,
284 });
285 }
286 COp::SetLocal { slot, value, add } => {
287 let Some(value) = eval(value, view) else {
288 return;
289 };
290 out.push(Effect::SetLocal {
291 slot: *slot,
292 value,
293 add: *add,
294 });
295 }
296 COp::SetTransform {
297 entity,
298 position,
299 rotation_deg,
300 scale,
301 } => {
302 let Some(entity) = eval(entity, view).and_then(Val::as_entity) else {
303 return;
304 };
305 let Some(mut transform) = (view.transforms)(entity) else {
306 return;
307 };
308 let field = |expr: &Option<CExpr>, into: &mut [f32; 3]| {
309 if let Some(expr) = expr
310 && let Some(v) = eval(expr, view).and_then(Val::as_vec3)
311 {
312 *into = v;
313 }
314 };
315 field(position, &mut transform.position);
316 field(rotation_deg, &mut transform.rotation_deg);
317 field(scale, &mut transform.scale);
318 out.push(Effect::SetTransform { entity, transform });
319 }
320 COp::Spawn {
321 template,
322 position,
323 rotation_deg,
324 scale,
325 lifetime,
326 bind,
327 } => {
328 out.push(Effect::Spawn(SpawnEffect {
329 template: *template,
330 transform: Transform {
331 position: *position,
332 rotation_deg: *rotation_deg,
333 scale: *scale,
334 },
335 lifetime: (*lifetime > 0.0).then_some(*lifetime),
336 }));
337 if let Some(bind) = bind {
340 set_binding(view, *bind, None);
341 }
342 }
343 COp::Despawn(target) => {
344 if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
345 out.push(Effect::Despawn(entity));
346 }
347 }
348 COp::Reparent { child, parent } => {
349 let Some(child) = eval(child, view).and_then(Val::as_entity) else {
350 return;
351 };
352 let parent = match parent {
355 Some(expr) => match eval(expr, view).and_then(Val::as_entity) {
356 Some(entity) => Some(entity),
357 None => return,
358 },
359 None => None,
360 };
361 out.push(Effect::Reparent { child, parent });
362 }
363 COp::Visible(target, visible) => {
364 if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
365 out.push(Effect::Visible(entity, *visible));
366 }
367 }
368 COp::Sound { clip, kind, volume } => out.push(Effect::Sound(PlayCue {
369 clip: *clip,
370 kind: *kind,
371 volume: *volume,
372 priority: 0,
373 })),
374 COp::Scene { scene, transition } => out.push(Effect::Scene {
375 scene: *scene,
376 transition: transition.clone(),
377 }),
378 COp::Screen(screen) => out.push(Effect::Screen(*screen)),
379 COp::Story(playback) => out.push(Effect::Story(*playback)),
380 COp::Save => out.push(Effect::Save),
381 COp::Never => {}
382 }
383}
384
385fn set_binding(view: &mut View<'_>, slot: u16, value: Option<Val>) {
386 if let Some(slot) = view.bindings.get_mut(slot as usize) {
387 *slot = value;
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::components::CueKind;
395 use crate::ecs::AudioClipHandle;
396 use alloc::boxed::Box;
397 use alloc::vec;
398 use core::num::NonZeroU32;
399
400 fn entity(index: u32) -> Entity {
401 Entity::new(index, NonZeroU32::MIN)
402 }
403
404 fn moved(position: [f32; 3]) -> Transform {
405 Transform {
406 position,
407 ..Transform::default()
408 }
409 }
410
411 #[derive(Default)]
416 struct Run {
417 dt: f32,
418 elapsed: f32,
419 vars: Vec<Val>,
420 locals: Vec<Val>,
421 queries: Vec<Vec<Entity>>,
422 entities: Vec<Entity>,
423 transforms: Vec<(Entity, Transform)>,
424 names: Vec<(AssetId, Entity)>,
425 self_entity: Option<Entity>,
426 bindings: usize,
427 }
428
429 impl Run {
430 fn with_view<R>(&self, body: impl FnOnce(&mut View<'_>) -> R) -> R {
431 let mut bindings = vec![None; self.bindings];
432 let mut trace = None;
433 let mut view = View {
434 dt: self.dt,
435 elapsed: self.elapsed,
436 vars: &self.vars,
437 locals: &self.locals,
438 bindings: &mut bindings,
439 queries: &self.queries,
440 by_name: &|id| self.names.iter().find(|(n, _)| *n == id).map(|(_, e)| *e),
441 transforms: &|e| {
442 self.transforms
443 .iter()
444 .find(|(t, _)| *t == e)
445 .map(|(_, t)| *t)
446 },
447 alive: &|e| self.entities.contains(&e),
448 self_entity: self.self_entity,
449 trace: &mut trace,
450 };
451 body(&mut view)
452 }
453
454 fn eval(&self, expr: &CExpr) -> Option<Val> {
455 self.with_view(|view| eval(expr, view))
456 }
457
458 fn exec(&self, nodes: &[CNode]) -> Vec<Effect> {
459 let mut out = Vec::new();
460 self.with_view(|view| exec(nodes, view, &mut out));
461 out
462 }
463 }
464
465 fn node(op: COp) -> CNode {
466 CNode { id: 0, op }
467 }
468
469 fn lit(v: Val) -> Box<CExpr> {
470 Box::new(CExpr::Lit(v))
471 }
472
473 #[test]
474 fn a_local_reads_its_slot_and_an_out_of_range_one_yields_nothing() {
475 let run = Run {
476 locals: vec![Val::Int(4), Val::Float(1.5)],
477 ..Run::default()
478 };
479 assert_eq!(run.eval(&CExpr::Local(1)), Some(Val::Float(1.5)));
480 assert_eq!(run.eval(&CExpr::Local(9)), None);
481 }
482
483 #[test]
484 fn dt_and_elapsed_read_the_tick() {
485 let run = Run {
486 dt: 0.25,
487 elapsed: 12.0,
488 ..Run::default()
489 };
490 assert_eq!(run.eval(&CExpr::Dt), Some(Val::Float(0.25)));
491 assert_eq!(run.eval(&CExpr::Elapsed), Some(Val::Float(12.0)));
492 }
493
494 #[test]
495 fn normalize_scales_to_unit_length() {
496 let run = Run::default();
497 let v = run
498 .eval(&CExpr::Normalize(lit(Val::Vec3([0.0, 3.0, 4.0]))))
499 .expect("a vector normalizes");
500 assert_eq!(v, Val::Vec3([0.0, 0.6, 0.8]));
501 }
502
503 #[test]
506 fn normalize_of_a_zero_vector_is_zero() {
507 let run = Run::default();
508 assert_eq!(
509 run.eval(&CExpr::Normalize(lit(Val::Vec3([0.0; 3])))),
510 Some(Val::Vec3([0.0; 3]))
511 );
512 }
513
514 #[test]
515 fn normalize_of_a_non_vector_yields_nothing() {
516 let run = Run::default();
517 assert_eq!(run.eval(&CExpr::Normalize(lit(Val::Int(3)))), None);
518 }
519
520 #[test]
521 fn not_inverts_a_bool_and_rejects_anything_else() {
522 let run = Run::default();
523 assert_eq!(
524 run.eval(&CExpr::Not(lit(Val::Bool(false)))),
525 Some(Val::Bool(true))
526 );
527 assert_eq!(run.eval(&CExpr::Not(lit(Val::Int(1)))), None);
528 }
529
530 #[test]
531 fn all_holds_only_when_every_operand_does() {
532 let run = Run::default();
533 let all = |items: Vec<CExpr>| run.eval(&CExpr::All(items));
534 assert_eq!(all(Vec::new()), Some(Val::Bool(true)));
535 assert_eq!(
536 all(vec![
537 CExpr::Lit(Val::Bool(true)),
538 CExpr::Lit(Val::Bool(true))
539 ]),
540 Some(Val::Bool(true))
541 );
542 assert_eq!(
543 all(vec![
544 CExpr::Lit(Val::Bool(true)),
545 CExpr::Lit(Val::Bool(false))
546 ]),
547 Some(Val::Bool(false))
548 );
549 assert_eq!(all(vec![CExpr::Lit(Val::Int(1))]), None);
550 }
551
552 #[test]
553 fn any_holds_as_soon_as_one_operand_does() {
554 let run = Run::default();
555 let any = |items: Vec<CExpr>| run.eval(&CExpr::Any(items));
556 assert_eq!(any(Vec::new()), Some(Val::Bool(false)));
557 assert_eq!(
558 any(vec![
559 CExpr::Lit(Val::Bool(false)),
560 CExpr::Lit(Val::Bool(true))
561 ]),
562 Some(Val::Bool(true))
563 );
564 assert_eq!(
565 any(vec![
566 CExpr::Lit(Val::Bool(false)),
567 CExpr::Lit(Val::Bool(false))
568 ]),
569 Some(Val::Bool(false))
570 );
571 assert_eq!(any(vec![CExpr::Lit(Val::Int(1))]), None);
572 }
573
574 #[test]
575 fn a_never_expression_yields_nothing() {
576 assert_eq!(Run::default().eval(&CExpr::Never), None);
577 }
578
579 #[test]
580 fn arithmetic_on_two_ints_stays_an_int() {
581 let run = Run::default();
582 let op = |op| run.eval(&CExpr::Arith(op, lit(Val::Int(7)), lit(Val::Int(2))));
583 assert_eq!(op(Arith::Add), Some(Val::Int(9)));
584 assert_eq!(op(Arith::Sub), Some(Val::Int(5)));
585 assert_eq!(op(Arith::Mul), Some(Val::Int(14)));
586 assert_eq!(op(Arith::Div), Some(Val::Int(3)));
587 }
588
589 #[test]
592 fn division_by_zero_yields_zero() {
593 let run = Run::default();
594 assert_eq!(
595 run.eval(&CExpr::Arith(
596 Arith::Div,
597 lit(Val::Int(7)),
598 lit(Val::Int(0))
599 )),
600 Some(Val::Int(0))
601 );
602 assert_eq!(
603 run.eval(&CExpr::Arith(
604 Arith::Div,
605 lit(Val::Vec3([1.0, 2.0, 3.0])),
606 lit(Val::Float(0.0)),
607 )),
608 Some(Val::Vec3([0.0; 3]))
609 );
610 }
611
612 #[test]
613 fn arithmetic_on_two_vectors_is_component_wise() {
614 let run = Run::default();
615 assert_eq!(
616 run.eval(&CExpr::Arith(
617 Arith::Add,
618 lit(Val::Vec3([1.0, 2.0, 3.0])),
619 lit(Val::Vec3([10.0, 20.0, 30.0])),
620 )),
621 Some(Val::Vec3([11.0, 22.0, 33.0]))
622 );
623 }
624
625 #[test]
626 fn a_vector_and_a_scalar_combine_component_wise_either_way_round() {
627 let run = Run::default();
628 assert_eq!(
629 run.eval(&CExpr::Arith(
630 Arith::Mul,
631 lit(Val::Vec3([1.0, 2.0, 3.0])),
632 lit(Val::Float(2.0)),
633 )),
634 Some(Val::Vec3([2.0, 4.0, 6.0]))
635 );
636 assert_eq!(
639 run.eval(&CExpr::Arith(
640 Arith::Sub,
641 lit(Val::Int(10)),
642 lit(Val::Vec3([1.0, 2.0, 3.0])),
643 )),
644 Some(Val::Vec3([9.0, 8.0, 7.0]))
645 );
646 }
647
648 #[test]
649 fn a_vector_against_a_non_numeric_yields_nothing() {
650 let run = Run::default();
651 assert_eq!(
652 run.eval(&CExpr::Arith(
653 Arith::Add,
654 lit(Val::Vec3([1.0; 3])),
655 lit(Val::Bool(true)),
656 )),
657 None
658 );
659 assert_eq!(
660 run.eval(&CExpr::Arith(
661 Arith::Add,
662 lit(Val::Bool(true)),
663 lit(Val::Vec3([1.0; 3])),
664 )),
665 None
666 );
667 }
668
669 #[test]
670 fn a_mixed_int_and_float_widens_to_a_float() {
671 let run = Run::default();
672 assert_eq!(
673 run.eval(&CExpr::Arith(
674 Arith::Add,
675 lit(Val::Int(1)),
676 lit(Val::Float(0.5)),
677 )),
678 Some(Val::Float(1.5))
679 );
680 assert_eq!(
681 run.eval(&CExpr::Arith(
682 Arith::Add,
683 lit(Val::Bool(true)),
684 lit(Val::Int(1)),
685 )),
686 None
687 );
688 }
689
690 #[test]
691 fn entities_and_bools_compare_by_identity_rather_than_as_numbers() {
692 let run = Run::default();
693 let cmp = |op, a, b| run.eval(&CExpr::Compare(op, lit(a), lit(b)));
694 let (a, b) = (Val::Entity(entity(1)), Val::Entity(entity(2)));
695 assert_eq!(cmp(Cmp::Eq, a, a), Some(Val::Bool(true)));
696 assert_eq!(cmp(Cmp::Eq, a, b), Some(Val::Bool(false)));
697 assert_eq!(cmp(Cmp::Ne, a, b), Some(Val::Bool(true)));
698 assert_eq!(
699 cmp(Cmp::Eq, Val::Bool(true), Val::Bool(true)),
700 Some(Val::Bool(true))
701 );
702 assert_eq!(cmp(Cmp::Lt, a, b), None);
705 }
706
707 #[test]
708 fn numbers_compare_in_every_ordering() {
709 let run = Run::default();
710 let cmp = |op| {
711 run.eval(&CExpr::Compare(op, lit(Val::Int(1)), lit(Val::Float(2.0))))
712 .and_then(Val::as_bool)
713 };
714 assert_eq!(cmp(Cmp::Eq), Some(false));
715 assert_eq!(cmp(Cmp::Ne), Some(true));
716 assert_eq!(cmp(Cmp::Lt), Some(true));
717 assert_eq!(cmp(Cmp::Le), Some(true));
718 assert_eq!(cmp(Cmp::Gt), Some(false));
719 assert_eq!(cmp(Cmp::Ge), Some(false));
720 }
721
722 #[test]
723 fn an_unevaluable_comparison_yields_nothing() {
724 let run = Run::default();
725 assert_eq!(
726 run.eval(&CExpr::Compare(
727 Cmp::Eq,
728 lit(Val::Bool(true)),
729 lit(Val::Int(1)),
730 )),
731 None
732 );
733 }
734
735 #[test]
736 fn a_condition_that_is_not_a_bool_runs_neither_branch() {
737 let run = Run::default();
738 let effects = run.exec(&[node(COp::If {
739 cond: CExpr::Lit(Val::Int(1)),
740 then: vec![node(COp::Save)],
741 otherwise: vec![node(COp::Save)],
742 })]);
743 assert!(effects.is_empty(), "{effects:?}");
744 }
745
746 #[test]
747 fn a_for_each_over_an_undeclared_query_runs_nothing() {
748 let run = Run {
749 bindings: 1,
750 ..Run::default()
751 };
752 let effects = run.exec(&[node(COp::ForEach {
753 query: 3,
754 bind: 0,
755 body: vec![node(COp::Save)],
756 })]);
757 assert!(effects.is_empty(), "{effects:?}");
758 }
759
760 #[test]
761 fn setting_a_local_from_an_unevaluable_value_asks_for_nothing() {
762 let run = Run::default();
763 let effects = run.exec(&[node(COp::SetLocal {
764 slot: 0,
765 value: CExpr::Never,
766 add: false,
767 })]);
768 assert!(effects.is_empty(), "{effects:?}");
769 }
770
771 #[test]
772 fn a_transform_write_skips_an_entity_it_cannot_resolve() {
773 let ghost = entity(7);
774 let run = Run::default();
776 let effects = run.exec(&[node(COp::SetTransform {
777 entity: CExpr::Never,
778 position: None,
779 rotation_deg: None,
780 scale: None,
781 })]);
782 assert!(effects.is_empty(), "{effects:?}");
783
784 let effects = run.exec(&[node(COp::SetTransform {
786 entity: CExpr::Lit(Val::Entity(ghost)),
787 position: None,
788 rotation_deg: None,
789 scale: None,
790 })]);
791 assert!(effects.is_empty(), "{effects:?}");
792 }
793
794 #[test]
795 fn a_transform_write_keeps_the_fields_it_was_not_given() {
796 let e = entity(1);
797 let run = Run {
798 transforms: vec![(e, moved([1.0, 2.0, 3.0]))],
799 ..Run::default()
800 };
801 let effects = run.exec(&[node(COp::SetTransform {
802 entity: CExpr::Lit(Val::Entity(e)),
803 position: Some(CExpr::Lit(Val::Vec3([9.0; 3]))),
804 rotation_deg: None,
807 scale: Some(CExpr::Lit(Val::Int(2))),
808 })]);
809 let [Effect::SetTransform { entity, transform }] = effects.as_slice() else {
810 panic!("expected one transform write, got {effects:?}");
811 };
812 assert_eq!(*entity, e);
813 assert_eq!(transform.position, [9.0; 3]);
814 assert_eq!(transform.scale, Transform::default().scale);
815 }
816
817 #[test]
818 fn a_reparent_skips_a_child_it_cannot_resolve() {
819 let run = Run::default();
820 let effects = run.exec(&[node(COp::Reparent {
821 child: CExpr::Never,
822 parent: None,
823 })]);
824 assert!(effects.is_empty(), "{effects:?}");
825 }
826
827 #[test]
830 fn a_reparent_onto_an_unresolvable_parent_skips_rather_than_detaching() {
831 let run = Run::default();
832 let effects = run.exec(&[node(COp::Reparent {
833 child: CExpr::Lit(Val::Entity(entity(1))),
834 parent: Some(CExpr::Never),
835 })]);
836 assert!(effects.is_empty(), "{effects:?}");
837 }
838
839 #[test]
840 fn a_reparent_moves_the_child_under_a_parent_or_to_the_root() {
841 let (child, parent) = (entity(1), entity(2));
842 let run = Run::default();
843 let effects = run.exec(&[
844 node(COp::Reparent {
845 child: CExpr::Lit(Val::Entity(child)),
846 parent: Some(CExpr::Lit(Val::Entity(parent))),
847 }),
848 node(COp::Reparent {
849 child: CExpr::Lit(Val::Entity(child)),
850 parent: None,
851 }),
852 ]);
853 let [
854 Effect::Reparent {
855 child: a,
856 parent: Some(p),
857 },
858 Effect::Reparent {
859 child: b,
860 parent: None,
861 },
862 ] = effects.as_slice()
863 else {
864 panic!("expected two reparents, got {effects:?}");
865 };
866 assert_eq!((*a, *p, *b), (child, parent, child));
867 }
868
869 #[test]
870 fn the_request_only_nodes_each_push_what_they_name() {
871 let run = Run::default();
872 let effects = run.exec(&[
873 node(COp::Sound {
874 clip: AudioClipHandle(3),
875 kind: CueKind::Music,
876 volume: 0.5,
877 }),
878 node(COp::Scene {
879 scene: AssetId(1),
880 transition: String::from("fade"),
881 }),
882 node(COp::Screen(AssetId(2))),
883 node(COp::Never),
886 ]);
887 let [
888 Effect::Sound(cue),
889 Effect::Scene { scene, transition },
890 Effect::Screen(screen),
891 ] = effects.as_slice()
892 else {
893 panic!("expected three requests, got {effects:?}");
894 };
895 assert_eq!(
896 (cue.clip, cue.kind, cue.volume),
897 (AudioClipHandle(3), CueKind::Music, 0.5)
898 );
899 assert_eq!(*scene, AssetId(1));
900 assert_eq!(transition, "fade");
901 assert_eq!(*screen, AssetId(2));
902 }
903}