Skip to main content

nightshade_api/
input.rs

1//! Keyboard, mouse, and time queries, plus the movement helpers every game
2//! rewrites.
3
4use nightshade::prelude::*;
5
6/// True while `key` is held down.
7#[inline]
8pub fn key_down(world: &World, key: KeyCode) -> bool {
9    world
10        .res::<nightshade::ecs::input::resources::Input>()
11        .keyboard
12        .is_key_pressed(key)
13}
14
15/// True only on the frame `key` went down.
16#[inline]
17pub fn key_pressed(world: &World, key: KeyCode) -> bool {
18    world
19        .res::<nightshade::ecs::input::resources::Input>()
20        .keyboard
21        .just_pressed(key)
22}
23
24/// True while the mouse button is held down.
25#[inline]
26pub fn mouse_down(world: &World, button: MouseButton) -> bool {
27    let state = world
28        .res::<nightshade::ecs::input::resources::Input>()
29        .mouse
30        .state;
31    match button {
32        MouseButton::Left => state.contains(MouseState::LEFT_CLICKED),
33        MouseButton::Right => state.contains(MouseState::RIGHT_CLICKED),
34        MouseButton::Middle => state.contains(MouseState::MIDDLE_CLICKED),
35        _ => false,
36    }
37}
38
39/// True only on the frame the mouse button went down.
40#[inline]
41pub fn mouse_clicked(world: &World, button: MouseButton) -> bool {
42    let state = world
43        .res::<nightshade::ecs::input::resources::Input>()
44        .mouse
45        .state;
46    match button {
47        MouseButton::Left => state.contains(MouseState::LEFT_JUST_PRESSED),
48        MouseButton::Right => state.contains(MouseState::RIGHT_JUST_PRESSED),
49        MouseButton::Middle => state.contains(MouseState::MIDDLE_JUST_PRESSED),
50        _ => false,
51    }
52}
53
54/// The cursor position in window pixels.
55#[inline]
56pub fn mouse_position(world: &World) -> Vec2 {
57    world
58        .res::<nightshade::ecs::input::resources::Input>()
59        .mouse
60        .position
61}
62
63/// How far the cursor moved this frame.
64#[inline]
65pub fn mouse_delta(world: &World) -> Vec2 {
66    world
67        .res::<nightshade::ecs::input::resources::Input>()
68        .mouse
69        .position_delta
70}
71
72/// How far the scroll wheel moved this frame: positive scrolling up or away,
73/// negative toward you, 0.0 when still.
74#[inline]
75pub fn mouse_scroll(world: &World) -> f32 {
76    world
77        .res::<nightshade::ecs::input::resources::Input>()
78        .mouse
79        .wheel_delta
80        .y
81}
82
83/// True when the cursor is over a retained UI widget. Gate world clicks on this
84/// so a press on a panel or button does not also place or pick in the scene.
85#[inline]
86pub fn pointer_over_ui(world: &World) -> bool {
87    world
88        .res::<nightshade::ecs::ui::resources::RetainedUiState>()
89        .interaction
90        .hovered_entity
91        .is_some()
92}
93
94/// A signed 1d input: -1.0 while `negative` is held, 1.0 while `positive` is
95/// held, 0.0 otherwise or when both are held.
96#[inline]
97pub fn axis(world: &World, negative: KeyCode, positive: KeyCode) -> f32 {
98    let mut value = 0.0;
99    if key_down(world, negative) {
100        value -= 1.0;
101    }
102    if key_down(world, positive) {
103        value += 1.0;
104    }
105    value
106}
107
108/// WASD as a camera relative movement direction on the ground plane,
109/// normalized so diagonals are not faster. Returns zero when nothing is held.
110/// Multiply by speed and [`delta_time`] and add to a position.
111pub fn wasd(world: &World) -> Vec3 {
112    let forward_input = axis(world, KeyCode::KeyS, KeyCode::KeyW);
113    let strafe_input = axis(world, KeyCode::KeyA, KeyCode::KeyD);
114    if forward_input == 0.0 && strafe_input == 0.0 {
115        return Vec3::zeros();
116    }
117    let camera_forward = crate::camera::camera_forward(world);
118    let mut planar_forward = Vec3::new(camera_forward.x, 0.0, camera_forward.z);
119    if planar_forward.magnitude_squared() < f32::EPSILON {
120        planar_forward = Vec3::new(0.0, 0.0, -1.0);
121    }
122    let planar_forward = planar_forward.normalize();
123    let right = nalgebra_glm::cross(&planar_forward, &Vec3::y());
124    (planar_forward * forward_input + right * strafe_input).normalize()
125}
126
127/// Seconds the previous frame took, scaled by the global time scale and zero
128/// while paused. Multiply rates by this for frame rate independent motion that
129/// honors slow motion, fast forward, and pause.
130#[inline]
131pub fn delta_time(world: &World) -> f32 {
132    world.res::<nightshade::ecs::time::Time>().delta_time
133}
134
135/// Seconds the previous frame took in real time, ignoring the global time scale
136/// and pause. Use it for work that must keep running while the game is paused or
137/// slowed, such as a pause menu animation.
138#[inline]
139pub fn real_delta_time(world: &World) -> f32 {
140    world.res::<nightshade::ecs::time::Time>().raw_delta_time
141}
142
143/// Seconds since the program started.
144#[inline]
145pub fn elapsed_seconds(world: &World) -> f32 {
146    world
147        .res::<nightshade::ecs::time::Time>()
148        .uptime_milliseconds as f32
149        / 1000.0
150}
151
152/// A gamepad button. `South`/`East`/`West`/`North` are the face buttons (A/B/X/Y
153/// on Xbox), with `LeftTrigger`/`RightTrigger` as the shoulder bumpers and
154/// `LeftTrigger2`/`RightTrigger2` as the analog triggers.
155#[cfg(feature = "gamepad")]
156pub use nightshade::prelude::gilrs::Button as GamepadButton;
157
158/// A gamepad analog axis: the two sticks (`LeftStickX`/`Y`, `RightStickX`/`Y`)
159/// and the trigger axes (`LeftZ`/`RightZ`).
160#[cfg(feature = "gamepad")]
161pub use nightshade::prelude::gilrs::Axis as GamepadAxis;
162
163/// True while `button` is held on the active gamepad.
164#[cfg(feature = "gamepad")]
165pub fn gamepad_button_down(world: &mut World, button: GamepadButton) -> bool {
166    nightshade::plugins::gamepad::with_active_gamepad(world, |gamepad| gamepad.is_pressed(button))
167        .unwrap_or(false)
168}
169
170/// True only on the frame `button` went down on the active gamepad.
171#[cfg(feature = "gamepad")]
172pub fn gamepad_button_pressed(world: &World, button: GamepadButton) -> bool {
173    world
174        .res::<nightshade::plugins::gamepad::Gamepad>()
175        .just_pressed(button)
176}
177
178/// An analog axis on the active gamepad, normalized to about -1.0 to 1.0.
179#[cfg(feature = "gamepad")]
180pub fn gamepad_axis(world: &mut World, axis: GamepadAxis) -> f32 {
181    nightshade::plugins::gamepad::with_active_gamepad(world, |gamepad| gamepad.value(axis))
182        .unwrap_or(0.0)
183}
184
185/// The left stick as a 2d vector, x to the right and y up.
186#[cfg(feature = "gamepad")]
187pub fn gamepad_left_stick(world: &mut World) -> Vec2 {
188    Vec2::new(
189        gamepad_axis(world, GamepadAxis::LeftStickX),
190        gamepad_axis(world, GamepadAxis::LeftStickY),
191    )
192}
193
194/// The right stick as a 2d vector, x to the right and y up.
195#[cfg(feature = "gamepad")]
196pub fn gamepad_right_stick(world: &mut World) -> Vec2 {
197    Vec2::new(
198        gamepad_axis(world, GamepadAxis::RightStickX),
199        gamepad_axis(world, GamepadAxis::RightStickY),
200    )
201}
202
203/// True when a gamepad is connected and active.
204#[cfg(feature = "gamepad")]
205pub fn gamepad_connected(world: &mut World) -> bool {
206    nightshade::plugins::gamepad::with_active_gamepad(world, |_gamepad| ()).is_some()
207}
208
209/// Maps a friendly button name to a gilrs button, so a string-based command and
210/// a language binding can name a button without the enum. Accepts the face
211/// letters (a/b/x/y or cross/circle/square/triangle) and the position names.
212#[cfg(feature = "gamepad")]
213pub(crate) fn gamepad_button_from_name(name: &str) -> Option<GamepadButton> {
214    Some(match name.to_ascii_lowercase().as_str() {
215        "south" | "a" | "cross" => GamepadButton::South,
216        "east" | "b" | "circle" => GamepadButton::East,
217        "west" | "x" | "square" => GamepadButton::West,
218        "north" | "y" | "triangle" => GamepadButton::North,
219        "left_shoulder" | "lb" => GamepadButton::LeftTrigger,
220        "right_shoulder" | "rb" => GamepadButton::RightTrigger,
221        "left_trigger" | "lt" => GamepadButton::LeftTrigger2,
222        "right_trigger" | "rt" => GamepadButton::RightTrigger2,
223        "select" | "back" => GamepadButton::Select,
224        "start" => GamepadButton::Start,
225        "mode" | "guide" => GamepadButton::Mode,
226        "left_thumb" | "l3" => GamepadButton::LeftThumb,
227        "right_thumb" | "r3" => GamepadButton::RightThumb,
228        "dpad_up" => GamepadButton::DPadUp,
229        "dpad_down" => GamepadButton::DPadDown,
230        "dpad_left" => GamepadButton::DPadLeft,
231        "dpad_right" => GamepadButton::DPadRight,
232        _ => return None,
233    })
234}
235
236/// Maps a friendly axis name to a gilrs axis, for the string-based command form.
237#[cfg(feature = "gamepad")]
238pub(crate) fn gamepad_axis_from_name(name: &str) -> Option<GamepadAxis> {
239    Some(match name.to_ascii_lowercase().as_str() {
240        "left_x" => GamepadAxis::LeftStickX,
241        "left_y" => GamepadAxis::LeftStickY,
242        "right_x" => GamepadAxis::RightStickX,
243        "right_y" => GamepadAxis::RightStickY,
244        "left_z" => GamepadAxis::LeftZ,
245        "right_z" => GamepadAxis::RightZ,
246        _ => return None,
247    })
248}