concinnity_asset/trigger_volume.rs
1// Trigger-volume schema: a spatial sensor region.
2
3use crate::{AssetId, PropCollider};
4
5/// An invisible sensor region that reports when something enters or leaves it.
6///
7/// A trigger volume senses overlap and never collides: nothing bounces off
8/// it and it blocks no movement. [Behavior](#behavior)s listen for its
9/// crossings with an `enter` or `exit` source, so "when the player steps into
10/// this area, open that door" is two declared assets. `detects` filters what
11/// sets it off: the player character, dynamic props, or anything. Volumes
12/// sense at their authored position; they do not move at runtime.
13///
14/// ```rust
15/// # use concinnity_asset::TriggerVolume;
16/// TriggerVolume {
17/// position: [4.0, 1.0, -2.0],
18/// ..Default::default()
19/// };
20/// ```
21#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
22#[serde(default)]
23pub struct TriggerVolume {
24 /// Asset identity; injected via `inject_name`. Not part of `args`.
25 #[serde(skip)]
26 pub asset_id: AssetId,
27 /// World-space position of the volume's center.
28 pub position: [f32; 3],
29 /// Euler rotation of the volume in degrees.
30 pub rotation_deg: [f32; 3],
31 /// The sensed region, in the same shape vocabulary as a
32 /// [PropCollider](#propcollider): a `cuboid` with `half_extents`, a `ball`
33 /// with `radius`, or a `capsule`.
34 pub collider: PropCollider,
35 /// What sets the volume off: the `player` character, dynamic `props`, or
36 /// `any` of them.
37 pub detects: TriggerFilter,
38}
39
40/// What a [TriggerVolume](#triggervolume) senses.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum TriggerFilter {
44 /// Only the player character (the controlled camera capsule or the
45 /// followed character).
46 #[default]
47 Player,
48 /// Only dynamic props (a `Prop` with a `PropBody`).
49 Props,
50 /// Anything the physics simulation moves.
51 Any,
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn blank_volume_is_a_unit_cuboid_sensing_the_player() {
60 let v: TriggerVolume = serde_json::from_str("{}").unwrap();
61 assert_eq!(v.collider.shape, "cuboid");
62 assert_eq!(v.collider.half_extents, [0.5, 0.5, 0.5]);
63 assert_eq!(v.detects, TriggerFilter::Player);
64 }
65
66 #[test]
67 fn filter_names_parse() {
68 let v: TriggerVolume = serde_json::from_str(r#"{"detects":"props"}"#).unwrap();
69 assert_eq!(v.detects, TriggerFilter::Props);
70 let v: TriggerVolume = serde_json::from_str(r#"{"detects":"any"}"#).unwrap();
71 assert_eq!(v.detects, TriggerFilter::Any);
72 }
73
74 #[test]
75 fn baked_round_trip_is_postcard_stable() {
76 let v: TriggerVolume = serde_json::from_str(
77 r#"{"position":[4,1,-2],"collider":{"shape":"ball","radius":2.0},"detects":"any"}"#,
78 )
79 .unwrap();
80 let bytes = postcard::to_allocvec(&v).unwrap();
81 let back: TriggerVolume = postcard::from_bytes(&bytes).unwrap();
82 assert_eq!(back.position, [4.0, 1.0, -2.0]);
83 assert_eq!(back.collider.shape, "ball");
84 assert_eq!(back.detects, TriggerFilter::Any);
85 }
86}