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(|¤t| 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}