Skip to main content

kvas_engine/
gameplay.rs

1//! Small built-in gameplay components usable from both the editor and native scripts.
2//!
3//! [`Tags`] provides non-unique group labels, [`Lifetime`] removes temporary
4//! subtrees, and [`Visibility`] hides a rendered subtree while gameplay continues.
5//!
6//! ```
7//! use kvas_engine::{scene::Scene, gameplay::{Tags, Lifetime}, runtime::Runtime};
8//! let mut scene = Scene::default();
9//! let effect = scene.spawn("temporary effect");
10//! let mut tags = Tags::default();
11//! assert!(tags.insert("effect"));
12//! scene.world.insert(effect, tags);
13//! scene.world.insert(effect, Lifetime::new(0.02).unwrap());
14//! let mut runtime = Runtime::new(scene, None);
15//! runtime.tick(0.03);
16//! assert!(!runtime.scene.world.is_alive(effect));
17//! ```
18
19use crate::components::Component;
20use crate::hierarchy;
21use crate::reflect::Field;
22use kvas_ecs::{Entity, World};
23use serde::{Deserialize, Serialize};
24
25/// Case-sensitive whitespace-separated entity group labels.
26/// Tags are non-unique across entities; tag strings cannot contain whitespace.
27/// Duplicate/extra whitespace entered in the Inspector is tolerated on reads.
28#[derive(Clone, Debug, Default, Serialize, Deserialize)]
29#[serde(default)]
30pub struct Tags {
31    /// Editable tag list, e.g. `enemy damageable patrol`.
32    pub value: String,
33}
34
35impl Tags {
36    /// Whether the exact tag is present. Empty strings never match.
37    pub fn contains(&self, tag: &str) -> bool {
38        self.iter().any(|current| current == tag)
39    }
40    /// Borrows tag tokens in stored order (manually entered duplicates are preserved).
41    pub fn iter(&self) -> impl Iterator<Item = &str> {
42        self.value.split_whitespace()
43    }
44    /// Inserts a nonempty tag without whitespace; returns false for invalid/duplicate tags.
45    pub fn insert(&mut self, tag: &str) -> bool {
46        if tag.is_empty() || tag.chars().any(char::is_whitespace) || self.contains(tag) {
47            return false;
48        }
49        if !self.value.is_empty() {
50            self.value.push(' ');
51        }
52        self.value.push_str(tag);
53        true
54    }
55    /// Removes every occurrence, normalizing remaining whitespace. Returns whether found.
56    pub fn remove(&mut self, tag: &str) -> bool {
57        if !self.contains(tag) {
58            return false;
59        }
60        self.value = self.iter().filter(|&current| current != tag).collect::<Vec<_>>().join(" ");
61        true
62    }
63}
64
65impl Component for Tags {
66    fn fields(&mut self) -> Vec<Field<'_>> {
67        vec![Field::text("tags", &mut self.value)]
68    }
69}
70
71/// Remaining simulation lifetime of an entity and its complete subtree.
72/// Updated once per accepted runtime frame after scripts. Pausing/time scaling
73/// applies naturally. Zero or invalid remaining time expires on the next update.
74#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
75#[serde(default)]
76pub struct Lifetime {
77    /// Remaining seconds; serialized so an edited scene can provide the initial lifetime.
78    pub seconds: f32,
79}
80
81impl Default for Lifetime {
82    fn default() -> Self {
83        Self { seconds: 5.0 }
84    }
85}
86
87impl Lifetime {
88    /// Creates a finite non-negative lifetime, or `None` for invalid input.
89    pub fn new(seconds: f32) -> Option<Self> {
90        (seconds.is_finite() && seconds >= 0.0).then_some(Self { seconds })
91    }
92}
93
94impl Component for Lifetime {
95    fn fields(&mut self) -> Vec<Field<'_>> {
96        vec![Field::f32("seconds", &mut self.seconds)]
97    }
98}
99
100/// Rendering visibility inherited by descendants; it does not disable scripts or physics.
101/// An absent component behaves as visible. A hidden ancestor hides all descendants,
102/// including transient procedural meshes. Lights remain active independently.
103#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
104#[serde(default)]
105pub struct Visibility {
106    /// Whether this entity allows its own and descendant meshes to render.
107    pub visible: bool,
108}
109
110impl Default for Visibility {
111    fn default() -> Self {
112        Self { visible: true }
113    }
114}
115
116impl Component for Visibility {
117    fn fields(&mut self) -> Vec<Field<'_>> {
118        vec![Field::bool("visible", &mut self.visible)]
119    }
120}
121
122/// Snapshot of all entities containing an exact tag, in entity index order.
123pub fn tagged(world: &World, tag: &str) -> Vec<Entity> {
124    let mut entities: Vec<_> = world.iter::<Tags>().filter(|(_, tags)| tags.contains(tag)).map(|(e, _)| e).collect();
125    entities.sort();
126    entities
127}
128
129/// Advances remaining lifetimes and removes expired subtrees before physics.
130/// Non-positive/non-finite deltas are ignored; malformed lifetimes expire on a valid update.
131pub fn update_lifetimes(world: &mut World, dt: f32) {
132    if !dt.is_finite() || dt <= 0.0 {
133        return;
134    }
135    let mut expired = Vec::new();
136    for (entity, lifetime) in world.iter_mut::<Lifetime>() {
137        lifetime.seconds -= dt;
138        if !lifetime.seconds.is_finite() || lifetime.seconds <= 0.0 {
139            expired.push(entity);
140        }
141    }
142    for entity in expired {
143        hierarchy::despawn_recursive(world, entity);
144    }
145}