1use alloc::boxed::Box;
5use alloc::string::String;
6use alloc::vec::Vec;
7
8use crate::components::StoryPlayback;
9use crate::ecs::AudioClipHandle;
10use crate::ecs::asset_id::AssetId;
11use crate::ecs::asset_id::de_opt_asset_ref;
12use crate::ecs::de_opt_audio_clip_handle;
13
14#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
31#[serde(default)]
32pub struct Behavior {
33 #[serde(skip)]
35 pub asset_id: AssetId,
36 pub on: BehaviorSource,
38 pub scope: Vec<String>,
49 pub locals: Vec<BehaviorLocal>,
52 pub queries: Vec<BehaviorQuery>,
54 #[serde(rename = "do")]
56 pub body: Vec<BehaviorNode>,
57 pub once: bool,
59 pub delay: f32,
62 pub cooldown: f32,
64}
65
66#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
68#[serde(rename_all = "lowercase")]
69pub enum BehaviorSource {
70 #[default]
72 Start,
73 Tick,
75 Timer {
78 #[serde(default)]
80 interval: f32,
81 #[serde(default)]
83 repeat: bool,
84 },
85 Variable(String),
87 Enter(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
89 Exit(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
91 Interact(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
94 Spawned,
96}
97
98#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
101#[serde(default)]
102pub struct BehaviorLocal {
103 pub name: String,
105 pub value: BehaviorLiteral,
107}
108
109#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
112#[serde(default)]
113pub struct BehaviorQuery {
114 pub name: String,
116 pub has: Vec<String>,
119}
120
121#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
123#[serde(rename_all = "lowercase")]
124pub enum BehaviorLiteral {
125 Bool(bool),
127 Int(i32),
129 Float(f32),
131 Vec3([f32; 3]),
133}
134
135impl Default for BehaviorLiteral {
136 fn default() -> Self {
137 BehaviorLiteral::Int(0)
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
147#[serde(rename_all = "lowercase")]
148pub enum BehaviorExpr {
149 Bool(bool),
151 Int(i32),
153 Float(f32),
155 Vec3([f32; 3]),
157 Var(String),
159 Local(String),
161 Bind(String),
163 Named(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
165 #[serde(rename = "self")]
167 SelfEntity,
168 Dt,
170 Elapsed,
172 Position(Box<BehaviorExpr>),
174 Distance(Box<BehaviorExpr>, Box<BehaviorExpr>),
176 First(String),
178 Count(String),
180 Alive(Box<BehaviorExpr>),
182 Add(Box<BehaviorExpr>, Box<BehaviorExpr>),
184 Sub(Box<BehaviorExpr>, Box<BehaviorExpr>),
186 Mul(Box<BehaviorExpr>, Box<BehaviorExpr>),
188 Div(Box<BehaviorExpr>, Box<BehaviorExpr>),
190 Normalize(Box<BehaviorExpr>),
192 Eq(Box<BehaviorExpr>, Box<BehaviorExpr>),
194 Ne(Box<BehaviorExpr>, Box<BehaviorExpr>),
196 Lt(Box<BehaviorExpr>, Box<BehaviorExpr>),
198 Le(Box<BehaviorExpr>, Box<BehaviorExpr>),
200 Gt(Box<BehaviorExpr>, Box<BehaviorExpr>),
202 Ge(Box<BehaviorExpr>, Box<BehaviorExpr>),
204 All(Vec<BehaviorExpr>),
206 Any(Vec<BehaviorExpr>),
208 Not(Box<BehaviorExpr>),
210}
211
212impl Default for BehaviorExpr {
213 fn default() -> Self {
214 BehaviorExpr::Bool(false)
215 }
216}
217
218#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum BehaviorNode {
225 If {
227 cond: BehaviorExpr,
229 #[serde(default)]
231 then: Vec<BehaviorNode>,
232 #[serde(default, rename = "else")]
234 otherwise: Vec<BehaviorNode>,
235 },
236 ForEach {
239 query: String,
241 bind: String,
243 #[serde(default, rename = "do")]
245 body: Vec<BehaviorNode>,
246 },
247 Let {
249 name: String,
251 value: BehaviorExpr,
253 },
254 Set {
257 var: String,
259 value: BehaviorExpr,
261 #[serde(default)]
263 add: bool,
264 },
265 SetLocal {
267 local: String,
269 value: BehaviorExpr,
271 #[serde(default)]
273 add: bool,
274 },
275 SetTransform {
277 entity: BehaviorExpr,
279 #[serde(default)]
281 position: Option<BehaviorExpr>,
282 #[serde(default)]
284 rotation_deg: Option<BehaviorExpr>,
285 #[serde(default)]
287 scale: Option<BehaviorExpr>,
288 },
289 Spawn {
292 #[serde(default, deserialize_with = "de_opt_asset_ref")]
294 template: Option<AssetId>,
295 #[serde(default)]
297 position: [f32; 3],
298 #[serde(default)]
300 rotation_deg: [f32; 3],
301 #[serde(default = "unit_scale")]
303 scale: [f32; 3],
304 #[serde(default)]
306 lifetime: f32,
307 #[serde(default)]
309 bind: Option<String>,
310 },
311 Despawn {
313 target: BehaviorExpr,
315 },
316 Reparent {
318 child: BehaviorExpr,
320 #[serde(default)]
322 parent: Option<BehaviorExpr>,
323 },
324 Show {
326 target: BehaviorExpr,
328 },
329 Hide {
332 target: BehaviorExpr,
334 },
335 Sound {
337 #[serde(default, deserialize_with = "de_opt_audio_clip_handle")]
339 clip: Option<AudioClipHandle>,
340 #[serde(default)]
342 kind: crate::components::CueKind,
343 #[serde(default = "unit_volume")]
345 volume: f32,
346 },
347 Scene {
349 #[serde(default, deserialize_with = "de_opt_asset_ref")]
351 scene: Option<AssetId>,
352 #[serde(default = "default_transition")]
354 transition: String,
355 },
356 Screen {
358 #[serde(default, deserialize_with = "de_opt_asset_ref")]
360 screen: Option<AssetId>,
361 },
362 Story(StoryPlayback),
364 Save,
371}
372
373impl Behavior {
374 pub fn plays_sound(&self) -> bool {
377 self.visit(&mut |n| matches!(n, BehaviorNode::Sound { .. }))
378 }
379
380 pub fn saves_state(&self) -> bool {
383 self.visit(&mut |n| matches!(n, BehaviorNode::Save))
384 }
385
386 pub fn is_scoped(&self) -> bool {
389 !self.scope.is_empty()
390 }
391
392 fn visit(&self, pred: &mut impl FnMut(&BehaviorNode) -> bool) -> bool {
394 fn walk(nodes: &[BehaviorNode], pred: &mut impl FnMut(&BehaviorNode) -> bool) -> bool {
395 nodes.iter().any(|n| {
396 pred(n)
397 || match n {
398 BehaviorNode::If {
399 then, otherwise, ..
400 } => walk(then, pred) || walk(otherwise, pred),
401 BehaviorNode::ForEach { body, .. } => walk(body, pred),
402 _ => false,
403 }
404 })
405 }
406 walk(&self.body, pred)
407 }
408}
409
410fn unit_scale() -> [f32; 3] {
411 [1.0, 1.0, 1.0]
412}
413
414fn unit_volume() -> f32 {
415 1.0
416}
417
418fn default_transition() -> String {
419 String::from("FadeBlack")
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 fn parse(json: &str) -> Behavior {
427 serde_json::from_str(json).expect("behavior parses")
428 }
429
430 fn as_if(node: &BehaviorNode) -> Option<(&BehaviorExpr, &[BehaviorNode], &[BehaviorNode])> {
433 match node {
434 BehaviorNode::If {
435 cond,
436 then,
437 otherwise,
438 } => Some((cond, then, otherwise)),
439 _ => None,
440 }
441 }
442
443 fn as_spawn(node: &BehaviorNode) -> Option<(Option<&str>, [f32; 3])> {
444 match node {
445 BehaviorNode::Spawn { bind, scale, .. } => Some((bind.as_deref(), *scale)),
446 _ => None,
447 }
448 }
449
450 fn as_set(node: &BehaviorNode) -> Option<(&str, &BehaviorExpr, bool)> {
451 match node {
452 BehaviorNode::Set { var, value, add } => Some((var, value, *add)),
453 _ => None,
454 }
455 }
456
457 fn as_despawn(node: &BehaviorNode) -> Option<&BehaviorExpr> {
458 match node {
459 BehaviorNode::Despawn { target } => Some(target),
460 _ => None,
461 }
462 }
463
464 fn as_lt(expr: &BehaviorExpr) -> Option<(&BehaviorExpr, &BehaviorExpr)> {
465 match expr {
466 BehaviorExpr::Lt(lhs, rhs) => Some((lhs, rhs)),
467 _ => None,
468 }
469 }
470
471 fn as_distance(expr: &BehaviorExpr) -> Option<(&BehaviorExpr, &BehaviorExpr)> {
472 match expr {
473 BehaviorExpr::Distance(a, b) => Some((a, b)),
474 _ => None,
475 }
476 }
477
478 #[test]
479 fn the_destructuring_helpers_only_match_their_own_kind() {
480 let b = parse(r#"{"do":[{"save":null}]}"#);
481 let save = &b.body[0];
482 assert!(as_if(save).is_none());
483 assert!(as_spawn(save).is_none());
484 assert!(as_set(save).is_none());
485 assert!(as_despawn(save).is_none());
486 assert!(as_lt(&BehaviorExpr::Bool(true)).is_none());
487 assert!(as_distance(&BehaviorExpr::Bool(true)).is_none());
488 }
489
490 #[test]
491 fn defaults_are_world_scoped_and_empty() {
492 let b = Behavior::default();
493 assert!(!b.is_scoped());
494 assert!(b.body.is_empty());
495 assert_eq!(b.on, BehaviorSource::Start);
496 assert!(!b.plays_sound());
497 assert!(!b.saves_state());
498 }
499
500 #[test]
501 fn tick_source_parses() {
502 let b = parse(r#"{"on":"tick"}"#);
503 assert_eq!(b.on, BehaviorSource::Tick);
504 }
505
506 #[test]
507 fn spawned_source_parses() {
508 let b = parse(r#"{"on":"spawned"}"#);
509 assert_eq!(b.on, BehaviorSource::Spawned);
510 }
511
512 #[test]
513 fn scope_and_locals_parse() {
514 let b = parse(
515 r#"{"on":"tick","scope":["Prop"],"locals":[{"name":"speed","value":{"float":3.0}}]}"#,
516 );
517 assert!(b.is_scoped());
518 assert_eq!(b.scope, ["Prop"]);
519 assert_eq!(b.locals.len(), 1);
520 assert_eq!(b.locals[0].name, "speed");
521 assert_eq!(b.locals[0].value, BehaviorLiteral::Float(3.0));
522 }
523
524 #[test]
525 fn query_parses() {
526 let b = parse(r#"{"queries":[{"name":"player","has":["Camera3D","Prop"]}]}"#);
527 assert_eq!(b.queries.len(), 1);
528 assert_eq!(b.queries[0].name, "player");
529 assert_eq!(b.queries[0].has, ["Camera3D", "Prop"]);
530 }
531
532 #[test]
533 fn comparison_expression_parses_as_pair() {
534 let b = parse(
535 r#"{"do":[{"if":{"cond":{"lt":[{"distance":["self",{"bind":"t"}]},{"float":20.0}]}}}]}"#,
536 );
537 let (cond, then, otherwise) = as_if(&b.body[0]).expect("an if node");
538 assert!(then.is_empty());
539 assert!(otherwise.is_empty());
540 let (lhs, rhs) = as_lt(cond).expect("a less-than comparison");
541 assert_eq!(*rhs, BehaviorExpr::Float(20.0));
542 let (a, b) = as_distance(lhs).expect("a distance expression");
543 assert_eq!(*a, BehaviorExpr::SelfEntity);
544 assert_eq!(*b, BehaviorExpr::Bind(String::from("t")));
545 }
546
547 #[test]
548 fn spawn_binds_the_new_entity() {
549 let b = parse(r#"{"do":[{"spawn":{"bind":"made"}}]}"#);
550 let (bind, scale) = as_spawn(&b.body[0]).expect("a spawn node");
551 assert_eq!(bind, Some("made"));
552 assert_eq!(scale, [1.0, 1.0, 1.0]);
553 }
554
555 #[test]
556 fn nested_nodes_are_visited_for_sound_and_save() {
557 let b = parse(
558 r#"{"do":[{"for_each":{"query":"q","bind":"e","do":[{"if":{"cond":{"bool":true},"then":[{"sound":{}}],"else":[{"save":null}]}}]}}]}"#,
559 );
560 assert!(b.plays_sound());
561 assert!(b.saves_state());
562 }
563
564 #[test]
565 fn round_trips_through_json() {
566 let b = parse(
567 r#"{"on":{"timer":{"interval":5.0,"repeat":true}},"do":[{"set":{"var":"visits","value":{"int":1},"add":true}}]}"#,
568 );
569 let encoded = serde_json::to_string(&b).expect("behavior encodes");
570 let again: Behavior = serde_json::from_str(&encoded).expect("behavior re-parses");
571 assert_eq!(again.on, b.on);
572 let (var, value, add) = as_set(&again.body[0]).expect("a set node");
573 assert_eq!(var, "visits");
574 assert_eq!(*value, BehaviorExpr::Int(1));
575 assert!(add);
576 }
577
578 #[test]
579 fn a_blank_expression_is_false() {
580 assert_eq!(BehaviorExpr::default(), BehaviorExpr::Bool(false));
583 }
584
585 #[test]
586 fn a_scene_node_fades_unless_told_to_cut() {
587 crate::test_support::install_resolvers();
588 let b = parse(r#"{"do":[{"scene":{"scene":"hub"}},{"scene":{"transition":"Cut"}}]}"#);
589 assert!(matches!(
590 (&b.body[0], &b.body[1]),
591 (
592 BehaviorNode::Scene { transition: a, .. },
593 BehaviorNode::Scene { transition: c, .. },
594 ) if a == "FadeBlack" && c == "Cut"
595 ));
596 }
597
598 #[test]
599 fn a_sound_node_plays_at_unit_gain_unless_told_otherwise() {
600 let b = parse(r#"{"do":[{"sound":{}}]}"#);
601 assert!(matches!(b.body[0], BehaviorNode::Sound { volume, .. } if volume == 1.0));
602 assert!(b.plays_sound());
603 }
604
605 #[test]
606 fn round_trips_through_postcard() {
607 let b = parse(r#"{"on":"tick","scope":["Prop"],"do":[{"despawn":{"target":"self"}}]}"#);
608 let bytes = postcard::to_allocvec(&b).expect("behavior encodes");
609 let again: Behavior = postcard::from_bytes(&bytes).expect("behavior decodes");
610 assert_eq!(again.on, BehaviorSource::Tick);
611 let target = as_despawn(&again.body[0]).expect("a despawn node");
612 assert_eq!(*target, BehaviorExpr::SelfEntity);
613 }
614}