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