mittens_engine/engine/ecs/component/
action.rs1use super::{Component, ComponentRef};
2use crate::engine::ecs::{ComponentId, IntentValue};
3
4#[derive(Debug, Clone)]
5pub struct ActionComponent {
6 pub signal: IntentValue,
10 pub target_sources: Vec<ComponentRef>,
14 pub resolved: bool,
19}
20
21impl ActionComponent {
22 pub fn new(signal: IntentValue) -> Self {
27 Self {
28 signal,
29 target_sources: Vec::new(),
30 resolved: true,
31 }
32 }
33
34 pub fn new_authored(signal: IntentValue, target_sources: Vec<ComponentRef>) -> Self {
39 Self {
40 signal,
41 target_sources,
42 resolved: false,
43 }
44 }
45
46 pub fn print(message: impl Into<String>) -> Self {
47 Self::new(IntentValue::Print {
48 message: message.into(),
49 })
50 }
51}
52
53impl Default for ActionComponent {
54 fn default() -> Self {
55 Self {
56 signal: IntentValue::Noop,
57 target_sources: Vec::new(),
58 resolved: true,
59 }
60 }
61}
62
63pub fn signal_target_slot_count(signal: &IntentValue) -> usize {
78 use IntentValue::*;
79 match signal {
80 Noop | Print { .. } => 0,
81
82 SetColor { component_ids, .. }
83 | SetText { component_ids, .. }
84 | SetEmissiveIntensity { component_ids, .. }
85 | SetPosition { component_ids, .. }
86 | LookAt { component_ids, .. }
87 | GLTFArmatureVisible { component_ids, .. }
88 | Detach { component_ids }
89 | RemoveSubtree { component_ids }
90 | AudioGraphRebuild { component_ids }
91 | RequestRaycast { component_ids }
92 | AudioLowPassSetCutoffHz { component_ids, .. }
93 | AudioBandPassSetCenterHz { component_ids, .. }
94 | OscillatorSetEnabled { component_ids, .. }
95 | OscillatorSetPitch { component_ids, .. }
96 | OscillatorScheduleSetPitch { component_ids, .. }
97 | AudioSchedulePlay { component_ids, .. }
98 | UpdateTransform { component_ids, .. } => component_ids.len(),
99
100 Attach { parents, .. } | AttachClone { parents, .. } => parents.len() + 1,
101 RemoveChild { parents, .. } | RemoveChildren { parents } => parents.len(),
102
103 _ => 0,
105 }
106}
107
108pub fn apply_resolved_targets(signal: &mut IntentValue, ids: &[ComponentId]) {
111 use IntentValue::*;
112 let mut cursor = 0usize;
113 let mut take = |n: usize| -> &[ComponentId] {
114 let slice = &ids[cursor..cursor + n];
115 cursor += n;
116 slice
117 };
118 match signal {
119 Noop | Print { .. } => {}
120
121 SetColor { component_ids, .. }
122 | SetText { component_ids, .. }
123 | SetEmissiveIntensity { component_ids, .. }
124 | SetPosition { component_ids, .. }
125 | LookAt { component_ids, .. }
126 | GLTFArmatureVisible { component_ids, .. }
127 | Detach { component_ids }
128 | RemoveSubtree { component_ids }
129 | AudioGraphRebuild { component_ids }
130 | RequestRaycast { component_ids }
131 | AudioLowPassSetCutoffHz { component_ids, .. }
132 | AudioBandPassSetCenterHz { component_ids, .. }
133 | OscillatorSetEnabled { component_ids, .. }
134 | OscillatorSetPitch { component_ids, .. }
135 | OscillatorScheduleSetPitch { component_ids, .. }
136 | AudioSchedulePlay { component_ids, .. }
137 | UpdateTransform { component_ids, .. } => {
138 let n = component_ids.len();
139 component_ids.copy_from_slice(take(n));
140 }
141
142 Attach { parents, child }
143 | AttachClone {
144 parents,
145 prefab_root: child,
146 } => {
147 let n = parents.len();
148 parents.copy_from_slice(take(n));
149 *child = take(1)[0];
150 }
151 RemoveChild { parents, .. } | RemoveChildren { parents } => {
152 let n = parents.len();
153 parents.copy_from_slice(take(n));
154 }
155
156 _ => {}
157 }
158 debug_assert_eq!(
159 cursor,
160 ids.len(),
161 "slot count mismatch in apply_resolved_targets"
162 );
163}
164
165impl Component for ActionComponent {
166 fn name(&self) -> &'static str {
167 "action"
168 }
169
170 fn as_any(&self) -> &dyn std::any::Any {
171 self
172 }
173
174 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
175 self
176 }
177
178 fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
179 emit.push_intent_now(
180 component,
181 crate::engine::ecs::IntentValue::RegisterAction {
182 component_ids: vec![component],
183 },
184 );
185 }
186
187 fn to_mms_ast(
188 &self,
189 _world: &crate::engine::ecs::World,
190 ) -> crate::scripting::ast::ComponentExpression {
191 use crate::engine::ecs::component::ce_helpers::*;
192 use crate::scripting::ast::Expression;
193
194 fn target_expr(t: &ComponentRef) -> Expression {
199 match t {
200 ComponentRef::Guid(u) => Expression::String(format!("@uuid:{u}")),
201 ComponentRef::Query(s) => Expression::String(s.clone()),
202 }
203 }
204 let targets_expr = |slice: &[ComponentRef]| -> Expression {
209 Expression::Array(slice.iter().map(target_expr).collect())
210 };
211
212 match &self.signal {
213 IntentValue::Noop => ce_call("Action", "noop", vec![]),
214 IntentValue::Print { message } => ce_call("Action", "print", vec![s(message)]),
215 IntentValue::SetColor { rgba, .. } => ce_call(
216 "Action",
217 "set_color",
218 vec![
219 targets_expr(&self.target_sources),
220 array(nums(rgba.iter().map(|&v| v as f64))),
221 ],
222 ),
223 IntentValue::SetText { text, .. } => ce_call(
224 "Action",
225 "set_text",
226 vec![targets_expr(&self.target_sources), s(text)],
227 ),
228 IntentValue::SetEmissiveIntensity { intensity, .. } => ce_call(
229 "Action",
230 "set_emissive_intensity",
231 vec![targets_expr(&self.target_sources), num(*intensity as f64)],
232 ),
233 IntentValue::SetPosition { position, .. } => ce_call(
234 "Action",
235 "set_position",
236 vec![
237 targets_expr(&self.target_sources),
238 array(nums(position.iter().map(|&v| v as f64))),
239 ],
240 ),
241 IntentValue::Attach { .. } => {
242 let (parents, child) = self
244 .target_sources
245 .split_at(self.target_sources.len().saturating_sub(1));
246 let child_expr = child.first().map(target_expr).unwrap_or_else(|| s(""));
247 ce_call("Action", "attach", vec![targets_expr(parents), child_expr])
248 }
249 IntentValue::AttachClone { .. } => {
250 let (parents, prefab) = self
251 .target_sources
252 .split_at(self.target_sources.len().saturating_sub(1));
253 let prefab_expr = prefab.first().map(target_expr).unwrap_or_else(|| s(""));
254 ce_call(
255 "Action",
256 "attach_clone",
257 vec![targets_expr(parents), prefab_expr],
258 )
259 }
260 IntentValue::Detach { .. } => {
261 ce_call("Action", "detach", vec![targets_expr(&self.target_sources)])
262 }
263 IntentValue::RemoveSubtree { .. } => ce_call(
264 "Action",
265 "remove_subtree",
266 vec![targets_expr(&self.target_sources)],
267 ),
268 IntentValue::RequestRaycast { .. } => ce_call(
269 "Action",
270 "request_raycast",
271 vec![targets_expr(&self.target_sources)],
272 ),
273 IntentValue::UpdateTransform {
274 translation,
275 rotation_quat_xyzw,
276 scale,
277 ..
278 } => {
279 let target_expr_ = self
283 .target_sources
284 .first()
285 .map(target_expr)
286 .unwrap_or_else(|| s(""));
287 ce_call(
288 "Action",
289 "update_transform_quat",
290 vec![
291 target_expr_,
292 array(nums(translation.iter().map(|&v| v as f64))),
293 array(nums(rotation_quat_xyzw.iter().map(|&v| v as f64))),
294 array(nums(scale.iter().map(|&v| v as f64))),
295 ],
296 )
297 }
298 _ => ce_call("Action", "noop", vec![]),
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use slotmap::{Key, KeyData};
310
311 fn cid(n: u64) -> ComponentId {
312 ComponentId::from(KeyData::from_ffi(n))
313 }
314
315 #[test]
316 fn slot_count_matches_apply_for_attach_variant() {
317 let mut iv = IntentValue::Attach {
318 parents: vec![ComponentId::null(), ComponentId::null()],
319 child: ComponentId::null(),
320 };
321 assert_eq!(signal_target_slot_count(&iv), 3);
322 apply_resolved_targets(&mut iv, &[cid(10), cid(11), cid(12)]);
323 let IntentValue::Attach { parents, child } = iv else {
324 unreachable!()
325 };
326 assert_eq!(parents, vec![cid(10), cid(11)]);
327 assert_eq!(child, cid(12));
328 }
329
330 #[test]
331 fn slot_count_matches_apply_for_vec_only_variant() {
332 let mut iv = IntentValue::SetColor {
333 component_ids: vec![ComponentId::null(), ComponentId::null()],
334 rgba: [1.0, 0.0, 0.0, 1.0],
335 };
336 assert_eq!(signal_target_slot_count(&iv), 2);
337 apply_resolved_targets(&mut iv, &[cid(7), cid(8)]);
338 let IntentValue::SetColor { component_ids, .. } = iv else {
339 unreachable!()
340 };
341 assert_eq!(component_ids, vec![cid(7), cid(8)]);
342 }
343
344 #[test]
345 fn no_slots_for_print() {
346 let iv = IntentValue::Print {
347 message: "hi".into(),
348 };
349 assert_eq!(signal_target_slot_count(&iv), 0);
350 }
351}