Skip to main content

kvas_engine/
script_ext.rs

1//! Convenience methods on [`Ctx`] for the things almost every script does: reading and setting the
2//! position, turning towards a target, moving, measuring distances and picking random values.
3//!
4//! These only call the public `Ctx` API, so they are plain shortcuts; use `ctx.scene.world`
5//! for anything they do not cover.
6//!
7//! ```
8//! use kvas_engine::prelude::*;
9//!
10//! #[derive(Default)]
11//! pub struct Chaser { speed: f32 }
12//! impl Script for Chaser {
13//!     fn update(&mut self, ctx: &mut Ctx) {
14//!         if let Some(player) = ctx.find("Player") {
15//!             if let Some(target) = ctx.position_of(player) {
16//!                 ctx.look_at(target);
17//!                 ctx.move_towards(target, self.speed);
18//!             }
19//!         }
20//!     }
21//! }
22//! ```
23
24use crate::components::Transform;
25use crate::hierarchy;
26use crate::script::Ctx;
27use glam::Vec3;
28use kvas_ecs::Entity;
29use std::sync::atomic::{AtomicU64, Ordering};
30
31static STREAM: AtomicU64 = AtomicU64::new(0x1234_5678_9ABC_DEF0);
32
33impl Ctx<'_> {
34    /// World position of this entity (alias of [`Ctx::world_position`]).
35    pub fn position(&self) -> Vec3 {
36        self.world_position()
37    }
38
39    /// World position of another entity, `None` when it is dead or has no [`Transform`].
40    pub fn position_of(&self, entity: Entity) -> Option<Vec3> {
41        if self.scene.world.is_alive(entity) && self.scene.world.has::<Transform>(entity) {
42            Some(hierarchy::global_matrix(&self.scene.world, entity).w_axis.truncate())
43        } else {
44            None
45        }
46    }
47
48    /// Moves this entity to a world position immediately (no physics teleport; see [`Ctx::teleport`]).
49    pub fn set_position(&mut self, position: Vec3) -> bool {
50        hierarchy::set_world_position(&mut self.scene.world, self.entity, position)
51    }
52
53    /// Adds `delta` to the local position (parent space axes).
54    pub fn translate(&mut self, delta: Vec3) {
55        self.transform_mut().position += delta;
56    }
57
58    /// Moves along the entity's own axes: `translate_local(Vec3::NEG_Z * 2.0)` goes 2 m forward.
59    pub fn translate_local(&mut self, delta: Vec3) {
60        let t = self.transform_mut();
61        let q = t.quat();
62        t.position += q * delta;
63    }
64
65    /// Adds Euler degrees (pitch, yaw, roll) to the local rotation.
66    pub fn rotate(&mut self, degrees: Vec3) {
67        self.transform_mut().rotation += degrees;
68    }
69
70    /// Facing direction (local -Z in parent space).
71    pub fn forward(&self) -> Vec3 {
72        self.transform().forward()
73    }
74
75    /// Right direction (local +X in parent space).
76    pub fn right(&self) -> Vec3 {
77        self.transform().right()
78    }
79
80    /// Up direction (local +Y in parent space).
81    pub fn up(&self) -> Vec3 {
82        self.transform().up()
83    }
84
85    /// Straight-line distance to another entity, `None` when it cannot be measured.
86    pub fn distance_to(&self, entity: Entity) -> Option<f32> {
87        self.position_of(entity).map(|p| p.distance(self.world_position()))
88    }
89
90    /// Unit vector from this entity to a world point, zero when they coincide.
91    pub fn direction_to(&self, target: Vec3) -> Vec3 {
92        (target - self.world_position()).normalize_or_zero()
93    }
94
95    /// Turns the entity (yaw and pitch, no roll) so that its forward axis points at a world point.
96    /// Assumes the parent is not rotated. Does nothing when the point is at the entity.
97    pub fn look_at(&mut self, target: Vec3) {
98        let d = self.direction_to(target);
99        if d == Vec3::ZERO {
100            return;
101        }
102        let yaw = (-d.x).atan2(-d.z).to_degrees();
103        let pitch = d.y.clamp(-1.0, 1.0).asin().to_degrees();
104        self.transform_mut().rotation = Vec3::new(pitch, yaw, 0.0);
105    }
106
107    /// Like [`Ctx::look_at`] but ignores height: only the yaw changes (characters, turrets).
108    pub fn look_at_flat(&mut self, target: Vec3) {
109        let mut d = target - self.world_position();
110        d.y = 0.0;
111        if d.length_squared() < 1e-8 {
112            return;
113        }
114        let yaw = (-d.x).atan2(-d.z).to_degrees();
115        self.transform_mut().rotation.y = yaw;
116    }
117
118    /// Walks towards a world point at `speed` metres per second (uses the callback `dt`).
119    /// Returns `true` once the point is reached. Moves the transform directly, so use it for
120    /// kinematic objects; dynamic bodies should use `add_impulse` or `set_velocity`.
121    pub fn move_towards(&mut self, target: Vec3, speed: f32) -> bool {
122        let here = self.world_position();
123        let delta = target - here;
124        let step = speed.max(0.0) * self.dt();
125        if delta.length() <= step.max(1e-5) {
126            self.set_position(target);
127            true
128        } else {
129            self.set_position(here + delta.normalize() * step);
130            false
131        }
132    }
133
134    /// The closest entity carrying `tag` (other than this one) and its distance.
135    pub fn nearest_tagged(&self, tag: &str) -> Option<(Entity, f32)> {
136        let here = self.world_position();
137        self.tagged(tag)
138            .into_iter()
139            .filter(|e| *e != self.entity)
140            .filter_map(|e| self.position_of(e).map(|p| (e, p.distance(here))))
141            .min_by(|a, b| a.1.total_cmp(&b.1))
142    }
143
144    /// Removes this entity (and its children) from the scene.
145    pub fn despawn_self(&mut self) {
146        let me = self.entity;
147        self.despawn(me);
148    }
149
150    /// A random number in `0.0..1.0`. Every call advances a shared stream, so values differ per call.
151    /// For a repeatable sequence keep a [`crate::random::Rng`] in your script instead.
152    pub fn random(&self) -> f32 {
153        let seed = STREAM.fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed) ^ self.time.frame ^ self.entity.to_bits();
154        crate::random::Rng::new(seed).f32()
155    }
156
157    /// A random number in `min..max`.
158    pub fn random_range(&self, min: f32, max: f32) -> f32 {
159        min + (max - min) * self.random()
160    }
161
162    /// True with probability `p` (0..1) on each call.
163    pub fn chance(&self, p: f32) -> bool {
164        self.random() < p
165    }
166}
167
168/// Hierarchy, tags and shared variables.
169impl Ctx<'_> {
170    /// The parent of this entity, if it has a living one.
171    pub fn parent(&self) -> Option<Entity> {
172        hierarchy::parent_of(&self.scene.world, self.entity)
173    }
174
175    /// The parent of any entity.
176    pub fn parent_of(&self, entity: Entity) -> Option<Entity> {
177        hierarchy::parent_of(&self.scene.world, entity)
178    }
179
180    /// The top-most ancestor (the entity itself when it has no parent).
181    pub fn root(&self) -> Entity {
182        let mut e = self.entity;
183        while let Some(p) = hierarchy::parent_of(&self.scene.world, e) {
184            e = p;
185        }
186        e
187    }
188
189    /// Every entity below `entity`: children, grandchildren and so on.
190    pub fn descendants(&self, entity: Entity) -> Vec<Entity> {
191        hierarchy::descendants(&self.scene.world, entity)
192    }
193
194    /// The first descendant of this entity with the given name.
195    pub fn find_child(&self, name: &str) -> Option<Entity> {
196        hierarchy::find_descendant(&self.scene.world, self.entity, name, false)
197    }
198
199    /// Entities carrying `tag` within `radius` metres of a world point, nearest first.
200    pub fn tagged_within(&self, tag: &str, center: Vec3, radius: f32) -> Vec<Entity> {
201        let mut found: Vec<(Entity, f32)> = self.tagged(tag).into_iter().filter_map(|e| self.position_of(e).map(|p| (e, p.distance(center)))).filter(|(_, d)| *d <= radius).collect();
202        found.sort_by(|a, b| a.1.total_cmp(&b.1));
203        found.into_iter().map(|(e, _)| e).collect()
204    }
205
206    /// All tags of an entity.
207    pub fn tags_of(&self, entity: Entity) -> Vec<String> {
208        self.scene.world.get::<crate::gameplay::Tags>(entity).map(|t| t.value.split_whitespace().map(str::to_string).collect()).unwrap_or_default()
209    }
210
211    /// Plays an animation clip on an object (cross-fading from the current one); adds an
212    /// [`crate::skeletal::Animator`] when the object has none. The model must have a skeleton:
213    /// clips from its file, or the procedural humanoid ones (`Idle`, `Walk`, `Run`, `Talk`,
214    /// `Wave`, `Sit`, `Wipe`, `Look`). Returns false for a stale entity.
215    pub fn play_animation(&mut self, entity: Entity, clip: &str) -> bool {
216        if !self.is_alive(entity) {
217            return false;
218        }
219        match self.scene.world.get_mut::<crate::skeletal::Animator>(entity) {
220            Some(a) => {
221                if a.clip != clip {
222                    a.clip = clip.to_string();
223                }
224                a.playing = true;
225            }
226            None => self.scene.world.insert(entity, crate::skeletal::Animator { clip: clip.to_string(), ..Default::default() }),
227        }
228        true
229    }
230
231    /// Playback speed of an object's animation (1 = as authored). Use it to match a walk cycle
232    /// to the speed the object really moves at.
233    pub fn set_animation_speed(&mut self, entity: Entity, speed: f32) {
234        if let Some(a) = self.scene.world.get_mut::<crate::skeletal::Animator>(entity) {
235            a.speed = speed.max(0.0);
236        }
237    }
238
239    /// Makes an animated humanoid go limp: its body becomes physics capsules joined at the
240    /// joints, starting in the current pose and pushed by `impulse` (newton-seconds, world space).
241    /// Adds a [`crate::ragdoll::Ragdoll`] when there is none. Returns false for a stale entity.
242    pub fn ragdoll(&mut self, entity: Entity, impulse: Vec3) -> bool {
243        if !self.is_alive(entity) {
244            return false;
245        }
246        match self.scene.world.get_mut::<crate::ragdoll::Ragdoll>(entity) {
247            Some(r) => {
248                r.active = true;
249                r.impulse = impulse;
250            }
251            None => self.scene.world.insert(entity, crate::ragdoll::Ragdoll { active: true, impulse, ..Default::default() }),
252        }
253        true
254    }
255
256    /// Ends a ragdoll: the bodies are removed and the clip animates the character again.
257    pub fn stop_ragdoll(&mut self, entity: Entity) {
258        if let Some(r) = self.scene.world.get_mut::<crate::ragdoll::Ragdoll>(entity) {
259            r.active = false;
260        }
261    }
262
263    /// Turns an animated humanoid's head (and chest) towards a world point, or lets go with
264    /// `None`. Adds a [`crate::ik::CharacterIk`] when there is none.
265    pub fn look_at_point(&mut self, entity: Entity, point: Option<Vec3>) {
266        match self.scene.world.get_mut::<crate::ik::CharacterIk>(entity) {
267            Some(ik) => ik.look_point = point,
268            None if point.is_some() => self.scene.world.insert(entity, crate::ik::CharacterIk { look_point: point, ..Default::default() }),
269            None => {}
270        }
271    }
272
273    /// The clips an animated object's model offers (empty until its first frame was skinned).
274    pub fn animation_clips(&self, entity: Entity) -> Vec<String> {
275        self.scene.world.get::<crate::skeletal::Animator>(entity).map(|a| a.clips.clone()).unwrap_or_default()
276    }
277
278    /// Stores a value that every script can read with [`Ctx::global`]. Lives until the game stops.
279    pub fn set_global(&mut self, key: &str, value: serde_json::Value) {
280        self.scene.globals.insert(key.to_string(), value);
281    }
282
283    /// A shared value, if it was set.
284    pub fn global(&self, key: &str) -> Option<&serde_json::Value> {
285        self.scene.globals.get(key)
286    }
287
288    /// A shared number (integers are converted), or `default`.
289    pub fn global_f32(&self, key: &str, default: f32) -> f32 {
290        self.global(key).and_then(|v| v.as_f64()).map_or(default, |v| v as f32)
291    }
292
293    /// A shared flag, or `default`.
294    pub fn global_bool(&self, key: &str, default: bool) -> bool {
295        self.global(key).and_then(|v| v.as_bool()).unwrap_or(default)
296    }
297
298    /// Adds to a shared number (starting from 0) and returns the new value: `ctx.add_global("score", 10.0)`.
299    pub fn add_global(&mut self, key: &str, delta: f32) -> f32 {
300        let value = self.global_f32(key, 0.0) + delta;
301        self.set_global(key, serde_json::json!(value));
302        value
303    }
304}
305
306/// Vertex access and deformation (see [`crate::mesh_deform`]).
307impl Ctx<'_> {
308    /// Makes the entity's built-in primitive mesh (`primitive:sphere`...) editable so its vertices
309    /// can be changed from scripts. Returns `true` when the entity is editable afterwards.
310    pub fn make_editable(&mut self, entity: Entity) -> bool {
311        use crate::components::MeshRenderer;
312        use crate::mesh::MeshData;
313        use crate::mesh_edit::EditableMesh;
314        if self.scene.world.has::<EditableMesh>(entity) {
315            return true;
316        }
317        let Some(name) = self.scene.world.get::<MeshRenderer>(entity).and_then(|m| m.mesh.strip_prefix("primitive:").map(str::to_string)) else { return false };
318        let Some(editable) = MeshData::primitive(&name).and_then(|m| EditableMesh::from_mesh(&m).ok()) else { return false };
319        self.scene.world.insert(entity, editable);
320        true
321    }
322
323    /// The editable mesh of an entity, for deformation: `ctx.editable(e)?.twist(Vec3::Y, 30.0)`.
324    pub fn editable(&mut self, entity: Entity) -> Option<&mut crate::mesh_edit::EditableMesh> {
325        self.scene.world.get_mut::<crate::mesh_edit::EditableMesh>(entity)
326    }
327}
328
329/// Logging for debugging: everything lands in the editor Console (warnings yellow, errors red).
330impl Ctx<'_> {
331    /// A yellow warning line: `ctx.warn("health is below zero")`.
332    pub fn warn(&mut self, message: impl Into<String>) {
333        self.log(format!("warning: {}", message.into()));
334    }
335
336    /// A red error line. The script keeps running; use it for conditions that should never happen.
337    pub fn error(&mut self, message: impl Into<String>) {
338        self.log(format!("error: {}", message.into()));
339    }
340
341    /// Logs `name = value` using `Debug`: `ctx.watch("velocity", ctx.velocity())`.
342    pub fn watch(&mut self, name: &str, value: impl std::fmt::Debug) {
343        self.log(format!("{name} = {value:?}"));
344    }
345
346    /// Like [`Ctx::watch`] but at most once per `seconds`, so it can sit in `update` without flooding the Console.
347    pub fn watch_every(&mut self, seconds: f32, name: &str, value: impl std::fmt::Debug) {
348        if self.every(seconds) {
349            self.watch(name, value);
350        }
351    }
352}