1use alloc::vec::Vec;
2use core::time::Duration;
3use std::collections::{HashMap, HashSet};
4
5use all_is_cubes::character::{self, Character};
6use all_is_cubes::euclid::{Point2D, Vector2D};
7use all_is_cubes::inv;
8use all_is_cubes::listen;
9use all_is_cubes::math::{FreeCoordinate, FreeVector, zo32};
10use all_is_cubes::physics;
11use all_is_cubes::time::Tick;
12use all_is_cubes::universe::{self, Handle, Universe};
13use all_is_cubes_render::camera::{
14 FogOption, LightingOption, NdcPoint2, NominalPixel, RenderMethod, TransparencyOption, Viewport,
15};
16
17use crate::apps::{ControlMessage, Settings};
18
19type MousePoint = Point2D<f64, NominalPixel>;
20
21#[derive(Debug)]
40pub struct InputProcessor {
41 keys_held: HashSet<Key>,
43 momentary_timeout: HashMap<Key, Duration>,
47 command_buffer: Vec<Key>,
50
51 mouselook_mode: listen::Cell<bool>,
55 has_pointer_lock: bool,
57
58 mouselook_buffer: Vector2D<FreeCoordinate, NominalPixel>,
60
61 mouse_ndc_position: Option<NdcPoint2>,
63
64 mouse_previous_pixel_position: Option<MousePoint>,
67}
68
69impl InputProcessor {
70 #[expect(
74 clippy::new_without_default,
75 reason = "I expect it'll grow some parameters"
76 )]
77 pub fn new() -> Self {
78 Self {
79 keys_held: HashSet::new(),
80 momentary_timeout: HashMap::new(),
81 command_buffer: Vec::new(),
82 mouselook_mode: listen::Cell::new(false), has_pointer_lock: false,
84 mouselook_buffer: Vector2D::zero(),
85 mouse_ndc_position: Some(NdcPoint2::origin()),
86 mouse_previous_pixel_position: None,
87 }
88 }
89
90 fn is_bound(key: Key) -> bool {
91 match key {
93 Key::Character('w') => true,
95 Key::Character('a') => true,
96 Key::Character('s') => true,
97 Key::Character('d') => true,
98 Key::Character('e') => true,
99 Key::Character('c') => true,
100 Key::Escape => true,
102 Key::Left => true,
103 Key::Right => true,
104 Key::Up => true,
105 Key::Down => true,
106 Key::Character(' ') => true,
107 Key::Character(d) if d.is_ascii_digit() => true,
108 Key::Character('i') => true,
109 Key::Character('l') => true,
110 Key::Character('o') => true,
111 Key::Character('p') => true,
112 Key::Character('u') => true,
113 Key::Character('y') => true,
114 Key::Character('`' | '~') => true,
115 _ => false,
116 }
117 }
118
119 fn is_command(key: Key) -> bool {
121 match key {
122 Key::Escape => true,
123 Key::Character(d) if d.is_ascii_digit() => true,
124 Key::Character('i') => true,
125 Key::Character('l') => true,
126 Key::Character('o') => true,
127 Key::Character('p') => true,
128 Key::Character('u') => true,
129 Key::Character('y') => true,
130 Key::Character('`' | '~') => true,
131 _ => false,
133 }
134 }
135
136 pub fn key_down(&mut self, key: Key) -> bool {
138 let bound = Self::is_bound(key);
139 if bound {
140 self.keys_held.insert(key);
141 if Self::is_command(key) {
142 self.command_buffer.push(key);
143 }
144 }
145 bound
146 }
147
148 pub fn key_up(&mut self, key: Key) {
150 self.keys_held.remove(&key);
151 }
152
153 pub fn key_momentary(&mut self, key: Key) -> bool {
156 self.momentary_timeout.insert(key, Duration::from_millis(200));
157 self.key_up(key);
158 self.key_down(key)
159 }
160
161 pub fn key_focus(&mut self, has_focus: bool) {
167 if has_focus {
168 } else {
170 self.keys_held.clear();
171 self.momentary_timeout.clear();
172
173 self.mouselook_mode.set(false);
174 }
175 }
176
177 pub fn wants_pointer_lock(&self) -> bool {
182 self.mouselook_mode.get()
183 }
184
185 pub fn has_pointer_lock(&mut self, value: bool) {
189 self.has_pointer_lock = value;
190 }
191
192 pub fn mouselook_delta(&mut self, delta: Vector2D<FreeCoordinate, NominalPixel>) {
199 if self.has_pointer_lock {
201 self.mouselook_buffer += delta * 0.2;
202 }
203 }
204
205 pub fn mouse_ndc_position(&mut self, position: Option<NdcPoint2>) {
216 self.mouse_ndc_position = position.filter(|p| p.x.abs() <= 1. && p.y.abs() <= 1.);
217 }
218
219 pub fn mouse_pixel_position(
232 &mut self,
233 viewport: Viewport,
234 position: Option<Point2D<f64, NominalPixel>>,
235 derive_movement: bool,
236 ) {
237 self.mouse_ndc_position(position.map(|p| viewport.normalize_nominal_point(p)));
238
239 if derive_movement {
240 if let (Some(p1), Some(p2)) = (self.mouse_previous_pixel_position, position) {
241 self.mouselook_delta((p2 - p1).map(FreeCoordinate::from));
242 }
243 self.mouse_previous_pixel_position = position;
244 } else {
245 self.mouse_previous_pixel_position = None;
246 }
247 }
248
249 pub fn movement(&self) -> FreeVector {
253 let mut vector = FreeVector::new(
254 self.net_movement(Key::Character('a'), Key::Character('d')),
255 self.net_movement(Key::Character('c'), Key::Character('e')),
256 self.net_movement(Key::Character('w'), Key::Character('s')),
257 );
258 if vector != FreeVector::zero() {
259 vector = vector.normalize();
260 }
261 vector
262 }
263
264 pub(crate) fn step(&mut self, tick: Tick) {
269 let mut to_drop = Vec::new();
270 for (key, duration) in self.momentary_timeout.iter_mut() {
271 if let Some(reduced) = duration.checked_sub(tick.delta_t()) {
272 *duration = reduced;
273 } else {
274 to_drop.push(*key);
275 }
276 }
277 for key in to_drop.drain(..) {
278 self.momentary_timeout.remove(&key);
279 self.key_up(key);
280 }
281
282 self.mouselook_buffer = Vector2D::zero();
283 }
284
285 pub(crate) fn apply_input(
290 &mut self,
291 targets: InputTargets<'_>,
292 tick: Tick,
293 ) -> Result<(), universe::HandleError> {
294 let InputTargets {
295 mut universe,
296 character: character_opt,
297 paused: paused_opt,
298 settings,
299 control_channel,
300 ui,
301 } = targets;
302
303 let _ = universe;
305
306 let dt = tick.delta_t().as_secs_f64();
307 let key_turning_step = 80.0 * dt;
308
309 if ui.is_some_and(|ui| ui.should_focus_on_ui()) && self.mouselook_mode.get() {
314 self.mouselook_mode.set(false)
315 }
316
317 if let (Some(universe), Some(character_handle)) = (&mut universe, character_opt) {
319 let movement = self.movement();
320
321 let turning = Vector2D::<_, ()>::new(
322 key_turning_step.mul_add(
323 self.net_movement(Key::Left, Key::Right),
324 self.mouselook_buffer.x,
325 ),
326 key_turning_step.mul_add(
327 self.net_movement(Key::Up, Key::Down),
328 self.mouselook_buffer.y,
329 ),
330 );
331
332 universe.mutate_component(character_handle, |body: &mut physics::Body| {
333 body.yaw = (body.yaw + turning.x).rem_euclid(360.0);
334 body.pitch = (body.pitch + turning.y).clamp(-90.0, 90.0);
335 })?;
336
337 universe.mutate_component(character_handle, |input: &mut character::Input| {
338 input.velocity_input = movement;
339 if self.keys_held.contains(&Key::Character(' ')) {
340 input.jump = true;
341 }
342 })?;
343 }
344
345 for key in self.command_buffer.drain(..) {
346 match key {
347 Key::Escape => {
348 if let Some(ch) = control_channel {
349 let _ = ch.try_send(ControlMessage::Back);
350 }
351 }
352 Key::Character('i') => {
353 if let Some(settings) = settings {
354 settings.mutate_graphics_options(|options| {
355 options.lighting_display = match options.lighting_display {
356 LightingOption::None => LightingOption::Flat,
357 LightingOption::Flat => LightingOption::Smooth,
358 LightingOption::Smooth => {
359 if options.render_method == RenderMethod::Reference {
362 LightingOption::Bounce
363 } else {
364 LightingOption::None
365 }
366 }
367 LightingOption::Bounce => LightingOption::None,
368 _ => LightingOption::None, };
370 });
371 }
372 }
373 Key::Character('l') => {
374 let new_state = !self.mouselook_mode.get();
376 self.mouselook_mode.set(new_state);
377 if new_state {
378 self.mouse_previous_pixel_position = None;
380 }
381 }
382 Key::Character('o') => {
383 if let Some(settings) = settings {
384 settings.mutate_graphics_options(|options| {
385 options.transparency = match options.transparency {
386 TransparencyOption::Surface => TransparencyOption::Volumetric,
387 TransparencyOption::Volumetric => {
388 TransparencyOption::Threshold(zo32(0.5))
389 }
390 TransparencyOption::Threshold(_) => TransparencyOption::Surface,
391 _ => TransparencyOption::Surface, };
393 });
394 }
395 }
396 Key::Character('p') => {
397 if let Some(paused) = paused_opt {
399 paused.update_mut(|p| *p = !*p);
400 }
401 }
402 Key::Character('u') => {
403 if let Some(settings) = settings {
404 settings.mutate_graphics_options(|options| {
405 options.fog = match options.fog {
406 FogOption::None => FogOption::Abrupt,
407 FogOption::Abrupt => FogOption::Compromise,
408 FogOption::Compromise => FogOption::Physical,
409 FogOption::Physical => FogOption::None,
410 _ => FogOption::None, };
412 });
413 }
414 }
415 Key::Character('y') => {
416 if let Some(settings) = settings {
417 settings.mutate_graphics_options(|options| {
418 options.render_method = match options.render_method {
419 RenderMethod::Mesh => RenderMethod::Reference,
420 RenderMethod::Reference => RenderMethod::Mesh,
421 _ => RenderMethod::Reference,
422 };
423 });
424 }
425 }
426 Key::Character('`' | '~') => {
427 if let Some(ch) = control_channel {
428 let _ = ch.try_send(ControlMessage::EnterDebug);
429 }
430 }
431 Key::Character(numeral) if numeral.is_ascii_digit() => {
432 let digit = numeral.to_digit(10).unwrap() as inv::Ix;
433 let slot = (digit + 9).rem_euclid(10); if let (Some(universe), Some(character_handle)) = (&mut universe, character_opt)
435 {
436 universe.mutate_component(
437 character_handle,
438 |c: &mut character::Input| {
439 c.set_selected_slots[1] = Some(slot);
440 },
441 )?;
442 }
443 }
444 _ => {}
445 }
446 }
447
448 Ok(())
449 }
450
451 pub fn mouselook_mode(&self) -> listen::DynSource<bool> {
456 self.mouselook_mode.as_source()
457 }
458
459 pub(crate) fn toggle_mouselook_mode(&mut self) {
460 self.set_mouselook_mode(!self.mouselook_mode.get());
461 }
462
463 pub fn set_mouselook_mode(&mut self, active: bool) {
465 let was_active = self.mouselook_mode.get();
467 self.mouselook_mode.set(active);
468 if active && !was_active {
469 self.mouse_previous_pixel_position = None;
471 }
472 }
473
474 pub fn cursor_ndc_position(&self) -> Option<NdcPoint2> {
480 if self.mouselook_mode.get() {
481 Some(NdcPoint2::origin())
482 } else {
483 self.mouse_ndc_position
484 }
485 }
486
487 fn net_movement(&self, negative: Key, positive: Key) -> FreeCoordinate {
489 match (
490 self.keys_held.contains(&negative),
491 self.keys_held.contains(&positive),
492 ) {
493 (true, false) => -1.0,
494 (false, true) => 1.0,
495 _ => 0.0,
496 }
497 }
498}
499
500#[derive(Debug, Default)]
505#[non_exhaustive]
506pub(crate) struct InputTargets<'a> {
507 pub universe: Option<&'a mut Universe>,
508 pub character: Option<&'a Handle<Character>>,
509 pub paused: Option<&'a listen::Cell<bool>>,
510 pub settings: Option<&'a Settings>,
511 pub control_channel: Option<&'a flume::Sender<ControlMessage>>,
514 pub ui: Option<&'a crate::ui_content::Vui>,
515}
516
517#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
519#[non_exhaustive]
520pub enum Key {
521 Character(char),
523 Escape,
525 Left,
527 Right,
529 Up,
531 Down,
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538 use all_is_cubes::euclid::vec3;
539 use all_is_cubes::space::Space;
540 use all_is_cubes::time;
541 use all_is_cubes::universe::{Handle, ReadTicket};
542
543 fn apply_input_helper(
544 input: &mut InputProcessor,
545 universe: &mut Universe,
546 character: &Handle<Character>,
547 ) {
548 input
549 .apply_input(
550 InputTargets {
551 universe: Some(universe),
552 character: Some(character),
553 paused: None,
554 settings: None,
555 control_channel: None,
556 ui: None,
557 },
558 Tick::arbitrary(),
559 )
560 .unwrap();
561 }
562
563 #[test]
564 fn movement() {
565 let mut input = InputProcessor::new();
566 assert_eq!(input.movement(), vec3(0.0, 0.0, 0.0));
567 input.key_down(Key::Character('d'));
568 assert_eq!(input.movement(), vec3(1.0, 0.0, 0.0));
569 input.key_down(Key::Character('a'));
570 assert_eq!(input.movement(), vec3(0.0, 0.0, 0.0));
571 input.key_up(Key::Character('d'));
572 assert_eq!(input.movement(), vec3(-1.0, 0.0, 0.0));
573 }
574
575 #[test]
576 fn focus_lost_cancels_keys() {
577 let mut input = InputProcessor::new();
578 assert_eq!(input.movement(), FreeVector::zero());
579 input.key_down(Key::Character('d'));
580 assert_eq!(input.movement(), vec3(1., 0., 0.));
581 input.key_focus(false);
582 assert_eq!(input.movement(), FreeVector::zero()); input.key_focus(true);
586 assert_eq!(input.movement(), FreeVector::zero());
587 input.key_down(Key::Character('d'));
588 assert_eq!(input.movement(), vec3(1., 0., 0.));
589 }
591
592 #[macro_rules_attribute::apply(smol_macros::test)]
595 async fn pause_menu_cancels_mouselook() {
596 let paused = listen::Cell::new(false);
597 let (cctx, _) = flume::bounded(1);
600 let mut ui = crate::ui_content::Vui::new(crate::ui_content::UiTargets {
601 mouselook_mode: listen::constant(false),
602 character_source: listen::constant(None),
603 paused: paused.as_source(),
604 graphics_options: listen::constant(Default::default()),
605 app_control_channel: cctx,
606 viewport_source: listen::constant(Viewport::ARBITRARY),
607 fullscreen_mode: listen::constant(None),
608 set_fullscreen: None,
609 quit: None,
610 custom_commands: listen::constant(Default::default()),
611 })
612 .await;
613
614 let mut input = InputProcessor::new();
616 input.toggle_mouselook_mode();
617 assert!(input.mouselook_mode().get());
618
619 input
621 .apply_input(
622 InputTargets {
623 ui: Some(&ui),
624 ..InputTargets::default()
625 },
626 Tick::arbitrary(),
627 )
628 .unwrap();
629 assert!(input.mouselook_mode().get());
630
631 paused.set(true);
633 ui.step(Tick::arbitrary(), time::Deadline::Asap, ReadTicket::stub());
634 input
635 .apply_input(
636 InputTargets {
637 ui: Some(&ui),
638 ..InputTargets::default()
639 },
640 Tick::arbitrary(),
641 )
642 .unwrap();
643 assert!(!input.mouselook_mode().get());
644 }
645
646 #[test]
647 fn slot_selection() {
648 let u = &mut Universe::new();
649 let space = u.insert_anonymous(Space::empty_positive(1, 1, 1));
650 let character =
651 u.insert("c".into(), Character::spawn_default(u.read_ticket(), space).unwrap()).unwrap();
652 let mut input = InputProcessor::new();
653
654 input.key_down(Key::Character('5'));
655 input.key_up(Key::Character('5'));
656 apply_input_helper(&mut input, u, &character);
657 u.step(false, time::Deadline::Whenever);
658 assert_eq!(
659 character.read(u.read_ticket()).unwrap().selected_slots()[1],
660 4
661 );
662
663 input.key_down(Key::Character('0'));
665 input.key_up(Key::Character('0'));
666 apply_input_helper(&mut input, u, &character);
667 u.step(false, time::Deadline::Whenever);
668 assert_eq!(
669 character.read(u.read_ticket()).unwrap().selected_slots()[1],
670 9
671 );
672 }
673
674 }