1use alloc::boxed::Box;
5use alloc::string::String;
6use alloc::vec::Vec;
7
8use crate::{AssetId, AudioClipHandle, StoryPlayback, de_opt_asset_ref, de_opt_audio_clip_handle};
9
10#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
27#[serde(default)]
28pub struct Behavior {
29 #[serde(skip)]
31 pub asset_id: AssetId,
32 pub on: BehaviorSource,
34 pub scope: Vec<String>,
45 pub locals: Vec<BehaviorLocal>,
48 pub queries: Vec<BehaviorQuery>,
50 #[serde(rename = "do")]
52 pub body: Vec<BehaviorNode>,
53 pub once: bool,
55 pub delay: f32,
58 pub cooldown: f32,
60}
61
62#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
64#[serde(rename_all = "lowercase")]
65pub enum BehaviorSource {
66 #[default]
68 Start,
69 Tick,
71 Timer {
74 #[serde(default)]
76 interval: f32,
77 #[serde(default)]
79 repeat: bool,
80 },
81 Variable(String),
83 Enter(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
85 Exit(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
87 Interact(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
90 Spawned,
92}
93
94#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
97#[serde(default)]
98pub struct BehaviorLocal {
99 pub name: String,
101 pub value: BehaviorLiteral,
103}
104
105#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
108#[serde(default)]
109pub struct BehaviorQuery {
110 pub name: String,
112 pub has: Vec<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum BehaviorLiteral {
121 Bool(bool),
123 Int(i32),
125 Float(f32),
127 Vec3([f32; 3]),
129}
130
131impl Default for BehaviorLiteral {
132 fn default() -> Self {
133 BehaviorLiteral::Int(0)
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
143#[serde(rename_all = "lowercase")]
144pub enum BehaviorExpr {
145 Bool(bool),
147 Int(i32),
149 Float(f32),
151 Vec3([f32; 3]),
153 Var(String),
155 Local(String),
157 Bind(String),
159 Named(#[serde(deserialize_with = "de_opt_asset_ref")] Option<AssetId>),
161 #[serde(rename = "self")]
163 SelfEntity,
164 Dt,
166 Elapsed,
168 Position(Box<BehaviorExpr>),
170 Distance(Box<BehaviorExpr>, Box<BehaviorExpr>),
172 First(String),
174 Count(String),
176 Alive(Box<BehaviorExpr>),
178 Add(Box<BehaviorExpr>, Box<BehaviorExpr>),
180 Sub(Box<BehaviorExpr>, Box<BehaviorExpr>),
182 Mul(Box<BehaviorExpr>, Box<BehaviorExpr>),
184 Div(Box<BehaviorExpr>, Box<BehaviorExpr>),
186 Normalize(Box<BehaviorExpr>),
188 Eq(Box<BehaviorExpr>, Box<BehaviorExpr>),
190 Ne(Box<BehaviorExpr>, Box<BehaviorExpr>),
192 Lt(Box<BehaviorExpr>, Box<BehaviorExpr>),
194 Le(Box<BehaviorExpr>, Box<BehaviorExpr>),
196 Gt(Box<BehaviorExpr>, Box<BehaviorExpr>),
198 Ge(Box<BehaviorExpr>, Box<BehaviorExpr>),
200 All(Vec<BehaviorExpr>),
202 Any(Vec<BehaviorExpr>),
204 Not(Box<BehaviorExpr>),
206}
207
208impl Default for BehaviorExpr {
209 fn default() -> Self {
210 BehaviorExpr::Bool(false)
211 }
212}
213
214#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum BehaviorNode {
221 If {
223 cond: BehaviorExpr,
225 #[serde(default)]
227 then: Vec<BehaviorNode>,
228 #[serde(default, rename = "else")]
230 otherwise: Vec<BehaviorNode>,
231 },
232 ForEach {
235 query: String,
237 bind: String,
239 #[serde(default, rename = "do")]
241 body: Vec<BehaviorNode>,
242 },
243 Let {
245 name: String,
247 value: BehaviorExpr,
249 },
250 Set {
253 var: String,
255 value: BehaviorExpr,
257 #[serde(default)]
259 add: bool,
260 },
261 SetLocal {
263 local: String,
265 value: BehaviorExpr,
267 #[serde(default)]
269 add: bool,
270 },
271 SetTransform {
273 entity: BehaviorExpr,
275 #[serde(default)]
277 position: Option<BehaviorExpr>,
278 #[serde(default)]
280 rotation_deg: Option<BehaviorExpr>,
281 #[serde(default)]
283 scale: Option<BehaviorExpr>,
284 },
285 Spawn {
288 #[serde(default, deserialize_with = "de_opt_asset_ref")]
290 template: Option<AssetId>,
291 #[serde(default)]
293 position: [f32; 3],
294 #[serde(default)]
296 rotation_deg: [f32; 3],
297 #[serde(default = "unit_scale")]
299 scale: [f32; 3],
300 #[serde(default)]
302 lifetime: f32,
303 #[serde(default)]
305 bind: Option<String>,
306 },
307 Despawn {
309 target: BehaviorExpr,
311 },
312 Reparent {
314 child: BehaviorExpr,
316 #[serde(default)]
318 parent: Option<BehaviorExpr>,
319 },
320 Show {
322 target: BehaviorExpr,
324 },
325 Hide {
328 target: BehaviorExpr,
330 },
331 Sound {
333 #[serde(default, deserialize_with = "de_opt_audio_clip_handle")]
335 clip: Option<AudioClipHandle>,
336 #[serde(default)]
338 kind: crate::CueKind,
339 #[serde(default = "unit_volume")]
341 volume: f32,
342 },
343 Scene {
345 #[serde(default, deserialize_with = "de_opt_asset_ref")]
347 scene: Option<AssetId>,
348 #[serde(default = "default_transition")]
350 transition: String,
351 },
352 Screen {
354 #[serde(default, deserialize_with = "de_opt_asset_ref")]
356 screen: Option<AssetId>,
357 },
358 Story(StoryPlayback),
360 Save,
367}
368
369impl Behavior {
370 pub fn plays_sound(&self) -> bool {
373 self.visit(&mut |n| matches!(n, BehaviorNode::Sound { .. }))
374 }
375
376 pub fn saves_state(&self) -> bool {
379 self.visit(&mut |n| matches!(n, BehaviorNode::Save))
380 }
381
382 pub fn is_scoped(&self) -> bool {
385 !self.scope.is_empty()
386 }
387
388 fn visit(&self, pred: &mut impl FnMut(&BehaviorNode) -> bool) -> bool {
390 fn walk(nodes: &[BehaviorNode], pred: &mut impl FnMut(&BehaviorNode) -> bool) -> bool {
391 nodes.iter().any(|n| {
392 pred(n)
393 || match n {
394 BehaviorNode::If {
395 then, otherwise, ..
396 } => walk(then, pred) || walk(otherwise, pred),
397 BehaviorNode::ForEach { body, .. } => walk(body, pred),
398 _ => false,
399 }
400 })
401 }
402 walk(&self.body, pred)
403 }
404}
405
406fn unit_scale() -> [f32; 3] {
407 [1.0, 1.0, 1.0]
408}
409
410fn unit_volume() -> f32 {
411 1.0
412}
413
414fn default_transition() -> String {
415 String::from("FadeBlack")
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 fn parse(json: &str) -> Behavior {
423 serde_json::from_str(json).expect("behavior parses")
424 }
425
426 fn as_if(node: &BehaviorNode) -> Option<(&BehaviorExpr, &[BehaviorNode], &[BehaviorNode])> {
429 match node {
430 BehaviorNode::If {
431 cond,
432 then,
433 otherwise,
434 } => Some((cond, then, otherwise)),
435 _ => None,
436 }
437 }
438
439 fn as_spawn(node: &BehaviorNode) -> Option<(Option<&str>, [f32; 3])> {
440 match node {
441 BehaviorNode::Spawn { bind, scale, .. } => Some((bind.as_deref(), *scale)),
442 _ => None,
443 }
444 }
445
446 fn as_set(node: &BehaviorNode) -> Option<(&str, &BehaviorExpr, bool)> {
447 match node {
448 BehaviorNode::Set { var, value, add } => Some((var, value, *add)),
449 _ => None,
450 }
451 }
452
453 fn as_despawn(node: &BehaviorNode) -> Option<&BehaviorExpr> {
454 match node {
455 BehaviorNode::Despawn { target } => Some(target),
456 _ => None,
457 }
458 }
459
460 fn as_lt(expr: &BehaviorExpr) -> Option<(&BehaviorExpr, &BehaviorExpr)> {
461 match expr {
462 BehaviorExpr::Lt(lhs, rhs) => Some((lhs, rhs)),
463 _ => None,
464 }
465 }
466
467 fn as_distance(expr: &BehaviorExpr) -> Option<(&BehaviorExpr, &BehaviorExpr)> {
468 match expr {
469 BehaviorExpr::Distance(a, b) => Some((a, b)),
470 _ => None,
471 }
472 }
473
474 #[test]
475 fn the_destructuring_helpers_only_match_their_own_kind() {
476 let b = parse(r#"{"do":[{"save":null}]}"#);
477 let save = &b.body[0];
478 assert!(as_if(save).is_none());
479 assert!(as_spawn(save).is_none());
480 assert!(as_set(save).is_none());
481 assert!(as_despawn(save).is_none());
482 assert!(as_lt(&BehaviorExpr::Bool(true)).is_none());
483 assert!(as_distance(&BehaviorExpr::Bool(true)).is_none());
484 }
485
486 #[test]
487 fn defaults_are_world_scoped_and_empty() {
488 let b = Behavior::default();
489 assert!(!b.is_scoped());
490 assert!(b.body.is_empty());
491 assert_eq!(b.on, BehaviorSource::Start);
492 assert!(!b.plays_sound());
493 assert!(!b.saves_state());
494 }
495
496 #[test]
497 fn tick_source_parses() {
498 let b = parse(r#"{"on":"tick"}"#);
499 assert_eq!(b.on, BehaviorSource::Tick);
500 }
501
502 #[test]
503 fn spawned_source_parses() {
504 let b = parse(r#"{"on":"spawned"}"#);
505 assert_eq!(b.on, BehaviorSource::Spawned);
506 }
507
508 #[test]
509 fn scope_and_locals_parse() {
510 let b = parse(
511 r#"{"on":"tick","scope":["Prop"],"locals":[{"name":"speed","value":{"float":3.0}}]}"#,
512 );
513 assert!(b.is_scoped());
514 assert_eq!(b.scope, ["Prop"]);
515 assert_eq!(b.locals.len(), 1);
516 assert_eq!(b.locals[0].name, "speed");
517 assert_eq!(b.locals[0].value, BehaviorLiteral::Float(3.0));
518 }
519
520 #[test]
521 fn query_parses() {
522 let b = parse(r#"{"queries":[{"name":"player","has":["Camera3D","Prop"]}]}"#);
523 assert_eq!(b.queries.len(), 1);
524 assert_eq!(b.queries[0].name, "player");
525 assert_eq!(b.queries[0].has, ["Camera3D", "Prop"]);
526 }
527
528 #[test]
529 fn comparison_expression_parses_as_pair() {
530 let b = parse(
531 r#"{"do":[{"if":{"cond":{"lt":[{"distance":["self",{"bind":"t"}]},{"float":20.0}]}}}]}"#,
532 );
533 let (cond, then, otherwise) = as_if(&b.body[0]).expect("an if node");
534 assert!(then.is_empty());
535 assert!(otherwise.is_empty());
536 let (lhs, rhs) = as_lt(cond).expect("a less-than comparison");
537 assert_eq!(*rhs, BehaviorExpr::Float(20.0));
538 let (a, b) = as_distance(lhs).expect("a distance expression");
539 assert_eq!(*a, BehaviorExpr::SelfEntity);
540 assert_eq!(*b, BehaviorExpr::Bind(String::from("t")));
541 }
542
543 #[test]
544 fn spawn_binds_the_new_entity() {
545 let b = parse(r#"{"do":[{"spawn":{"bind":"made"}}]}"#);
546 let (bind, scale) = as_spawn(&b.body[0]).expect("a spawn node");
547 assert_eq!(bind, Some("made"));
548 assert_eq!(scale, [1.0, 1.0, 1.0]);
549 }
550
551 #[test]
552 fn nested_nodes_are_visited_for_sound_and_save() {
553 let b = parse(
554 r#"{"do":[{"for_each":{"query":"q","bind":"e","do":[{"if":{"cond":{"bool":true},"then":[{"sound":{}}],"else":[{"save":null}]}}]}}]}"#,
555 );
556 assert!(b.plays_sound());
557 assert!(b.saves_state());
558 }
559
560 #[test]
561 fn round_trips_through_json() {
562 let b = parse(
563 r#"{"on":{"timer":{"interval":5.0,"repeat":true}},"do":[{"set":{"var":"visits","value":{"int":1},"add":true}}]}"#,
564 );
565 let encoded = serde_json::to_string(&b).expect("behavior encodes");
566 let again: Behavior = serde_json::from_str(&encoded).expect("behavior re-parses");
567 assert_eq!(again.on, b.on);
568 let (var, value, add) = as_set(&again.body[0]).expect("a set node");
569 assert_eq!(var, "visits");
570 assert_eq!(*value, BehaviorExpr::Int(1));
571 assert!(add);
572 }
573
574 #[test]
575 fn a_blank_expression_is_false() {
576 assert_eq!(BehaviorExpr::default(), BehaviorExpr::Bool(false));
579 }
580
581 #[test]
582 fn a_scene_node_fades_unless_told_to_cut() {
583 crate::test_support::install_resolvers();
584 let b = parse(r#"{"do":[{"scene":{"scene":"hub"}},{"scene":{"transition":"Cut"}}]}"#);
585 assert!(matches!(
586 (&b.body[0], &b.body[1]),
587 (
588 BehaviorNode::Scene { transition: a, .. },
589 BehaviorNode::Scene { transition: c, .. },
590 ) if a == "FadeBlack" && c == "Cut"
591 ));
592 }
593
594 #[test]
595 fn a_sound_node_plays_at_unit_gain_unless_told_otherwise() {
596 let b = parse(r#"{"do":[{"sound":{}}]}"#);
597 assert!(matches!(b.body[0], BehaviorNode::Sound { volume, .. } if volume == 1.0));
598 assert!(b.plays_sound());
599 }
600
601 #[test]
602 fn round_trips_through_postcard() {
603 let b = parse(r#"{"on":"tick","scope":["Prop"],"do":[{"despawn":{"target":"self"}}]}"#);
604 let bytes = postcard::to_allocvec(&b).expect("behavior encodes");
605 let again: Behavior = postcard::from_bytes(&bytes).expect("behavior decodes");
606 assert_eq!(again.on, BehaviorSource::Tick);
607 let target = as_despawn(&again.body[0]).expect("a despawn node");
608 assert_eq!(*target, BehaviorExpr::SelfEntity);
609 }
610}