-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinput.rs
More file actions
283 lines (264 loc) · 12.8 KB
/
Copy pathinput.rs
File metadata and controls
283 lines (264 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
//! Named input actions + bindings.
//!
//! Game code never calls `is_key_pressed(KeyCode::X)` directly.
//! Every keyboard check goes through one of the helpers here, keyed
//! off a named `Action`. Rebinding becomes a one-place edit; the
//! pause menu's currently-stubbed Keybindings entry can later let
//! the player swap underlying `KeyCode`s without touching gameplay.
//!
//! ## Why this is here
//!
//! Direct key checks scattered through code make remapping,
//! gamepad support, and accessibility options expensive to add
//! later. Routing through actions decouples the *intent*
//! ("attack", "interact") from the *input device* (keyboard,
//! mouse, future gamepad). When a `KeyCode` reference shows up
//! anywhere outside this module, the action layer was bypassed
//! and the ergonomic story breaks.
//!
//! ## What lives here vs. what doesn't
//!
//! - **Keyboard actions:** all of them. If a key has gameplay
//! semantics (movement, attack, menu nav, confirm, etc.) it's an
//! `Action` variant.
//! - **Mouse:** still handled directly via `macroquad::input` in
//! call sites — this module is keyboard-only for now. A future
//! pass can broaden `Action` to mouse buttons too.
//! - **Letter selection** (a-z mapped to inventory / category
//! slots): exposed via `letter_pressed(idx)` so call sites don't
//! poke `KeyCode::A` etc. directly.
//! - **Char input** during the save-name prompt: still uses
//! macroquad's `get_char_pressed` — chars aren't actions, they
//! *are* the data being typed.
//!
//! ## Adding a new action
//!
//! 1. Add a variant to [`Action`].
//! 2. Bind it in [`Bindings::default_keyboard`] to one or more
//! `KeyCode`s.
//! 3. Call `bindings.pressed(Action::Whatever)` at the use site.
//!
//! Bindings are a `Vec<KeyCode>` per action, so multiple keys can
//! all trigger the same action (arrows + WASD + numpad).
use std::collections::HashMap;
use macroquad::input::{is_key_down, is_key_pressed, KeyCode};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Action {
// ── Movement & menu navigation ──────────────────────────
// The same keys serve both — context (overlay open vs not)
// determines which interpretation fires. Splitting into
// separate actions later (e.g. "WASD only for movement,
// arrows only for menus") is a one-binding-table edit.
Up,
Down,
Left,
Right,
// ── Turn-spending actions ───────────────────────────────
/// Pass / wait one turn without moving or attacking.
Wait,
/// Bump-action interaction with the prop / door in front.
Interact,
/// Stomp / kick the adjacent enemy.
Kick,
/// Class signature ability — currently Security's combat stims.
/// Other classes route their signature here too once authored.
SignatureAbility,
/// Swap the right-hand weapon with whatever's stashed in the
/// `ReadyWeapon` slot. Costs one player turn; lets the player
/// flip between a wielded melee and a stashed ranged (or vice
/// versa) cheaply.
SwapWeapon,
/// Enter keyboard aiming mode for the equipped ranged weapon.
/// Released-fire: a second F (or Enter) commits the shot;
/// arrows / Tab move the cursor; Esc cancels without a turn cost.
FireAim,
/// Enter keyboard aiming mode for the assigned `Throwable` slot.
/// Same control surface as FireAim, but resolves to a thrown-item
/// action whose impact radius and range are item-driven.
ThrowAim,
/// Cycle through valid targets while in an aiming overlay.
/// Outside aim mode the key is free — see `CycleTarget`'s binding
/// in `Bindings::default_keyboard` and the aim-frame input
/// handler in `main.rs`.
CycleTarget,
// ── UI overlay toggles ──────────────────────────────────
ToggleInventory,
ToggleEquipment,
ToggleCrafting,
// ── Confirm / cancel ────────────────────────────────────
Confirm,
Cancel,
// ── In-overlay actions ──────────────────────────────────
/// Use / activate / consume the focused inventory entry.
Use,
/// Equip the focused entry (or, in the loot screen, equip the
/// item being taken).
Equip,
/// Stash a weapon into the `ReadyWeapon` slot directly from
/// the inventory's item-action prompt. Routes to the same
/// `equip_from_inventory_into` helper as the equipment-screen
/// belt-pouch flow, just targeting `EquipSlot::ReadyWeapon`.
Ready,
/// Drain every item from a container into inventory at once.
TakeAll,
/// Re-roll the stats block on the rollstats screen.
Reroll,
/// Focus / commit the highlighted recipe in the crafting screen.
Focus,
// ── Text entry ──────────────────────────────────────────
/// Backspace in the save-name prompt.
DeleteChar,
}
/// Action → keys lookup. A `Vec<KeyCode>` per action lets one
/// action accept several physical keys (e.g. arrow + WASD + numpad
/// all pointing at `Action::Up`). Two different actions sharing a
/// key is fine and intentional — `Reroll` and `TakeAll` both bind
/// `R` because they live in different overlays and the contextually-
/// open overlay decides which one fires.
pub struct Bindings {
map: HashMap<Action, Vec<KeyCode>>,
}
impl Bindings {
/// Vanilla keyboard layout. Tweak here to retune defaults; the
/// pause-menu Keybindings entry will eventually let the player
/// override per-action without recompiling.
pub fn default_keyboard() -> Self {
use Action::*;
let mut m: HashMap<Action, Vec<KeyCode>> = HashMap::new();
// Movement / navigation share keys. WASD and numpad both
// mirror the arrow keys so roguelike + first-person muscle
// memory both work.
m.insert(Up, vec![KeyCode::Up, KeyCode::W, KeyCode::Kp8]);
m.insert(Down, vec![KeyCode::Down, KeyCode::S, KeyCode::Kp2]);
m.insert(Left, vec![KeyCode::Left, KeyCode::A, KeyCode::Kp4]);
m.insert(Right, vec![KeyCode::Right, KeyCode::D, KeyCode::Kp6]);
// Turn-spending. Period + Space + Kp5 all rest in place;
// Space is a comfortable thumb key on a standard keyboard
// and Kp5 matches the roguelike numpad-rest convention.
m.insert(Wait, vec![KeyCode::Period, KeyCode::Space, KeyCode::Kp5]);
m.insert(Interact, vec![KeyCode::E]);
m.insert(Kick, vec![KeyCode::K]);
// Z was the Combat-Stims (Security signature) key; moved to
// X so Z can carry the more frequently-used SwapWeapon
// action for all classes. Stims still gate on player class
// at the use site.
m.insert(SignatureAbility, vec![KeyCode::X]);
m.insert(SwapWeapon, vec![KeyCode::Z]);
// Keyboard-aimed combat. F enters fire mode for ranged
// weapons; T enters throw mode for the assigned Throwable.
// Both keys do double duty — F is also `Focus` inside the
// crafting screen, T is `TakeAll` inside the loot screen.
// Those overlays gate their handlers behind their own
// `show_*` flags, so the same physical key drives different
// actions in different contexts without collision.
m.insert(FireAim, vec![KeyCode::F]);
m.insert(ThrowAim, vec![KeyCode::T]);
m.insert(CycleTarget, vec![KeyCode::Tab]);
// UI toggles.
m.insert(ToggleInventory, vec![KeyCode::I]);
m.insert(ToggleEquipment, vec![KeyCode::Tab]);
m.insert(ToggleCrafting, vec![KeyCode::C]);
// Menu primitives.
m.insert(Confirm, vec![KeyCode::Enter]);
m.insert(Cancel, vec![KeyCode::Escape]);
// In-overlay actions.
m.insert(Use, vec![KeyCode::U]);
m.insert(Equip, vec![KeyCode::Q]);
m.insert(Ready, vec![KeyCode::R]);
m.insert(TakeAll, vec![KeyCode::R, KeyCode::T]);
m.insert(Reroll, vec![KeyCode::R, KeyCode::Space]);
m.insert(Focus, vec![KeyCode::F]);
// Text entry.
m.insert(DeleteChar, vec![KeyCode::Backspace]);
Self { map: m }
}
/// Keys currently bound to `action`. Empty slice if nothing's
/// bound — actions without bindings are silently inactive
/// rather than panicking, so a future "unbind" path doesn't
/// have to guard against missing keys at every call site.
pub fn keys(&self, action: Action) -> &[KeyCode] {
self.map.get(&action).map(|v| v.as_slice()).unwrap_or(&[])
}
/// True if any key bound to `action` was pressed *this frame*
/// (rising edge). The standard "did the player just hit the
/// button" check.
pub fn pressed(&self, action: Action) -> bool {
self.keys(action).iter().any(|&k| is_key_pressed(k))
}
/// True if any key bound to `action` is currently held down.
/// Doesn't account for the input-suppression list; for
/// gameplay-movement gating use [`down_active`] instead.
pub fn down(&self, action: Action) -> bool {
self.keys(action).iter().any(|&k| is_key_down(k))
}
/// Held-state with suppression. A key in `suppressed` is treated
/// as if it weren't held — used by the "released-since-overlay-
/// closed" gate so a movement key still down from before an
/// overlay closed doesn't immediately trigger movement.
pub fn down_active(&self, action: Action, suppressed: &[KeyCode]) -> bool {
self.keys(action).iter()
.any(|&k| is_key_down(k) && !suppressed.contains(&k))
}
/// Every distinct key bound to any of the four cardinal
/// movement actions. Used by the overlay-close path to latch
/// movement keys into the suppression list so a held key
/// doesn't bleed into a step the moment a menu closes.
pub fn movement_keys(&self) -> Vec<KeyCode> {
let mut out = Vec::new();
for a in [Action::Up, Action::Down, Action::Left, Action::Right] {
for &k in self.keys(a) {
if !out.contains(&k) { out.push(k); }
}
}
out
}
}
/// Did the player press a number-row digit key for index `idx`
/// (0 = `1`, 1 = `2`, ..., 8 = `9`)? Used by the inventory's
/// category-collapse hotkeys. Same role as [`letter_pressed`] but
/// for the digit row.
pub fn digit_pressed(idx: usize) -> bool {
const KEYS: [KeyCode; 9] = [
KeyCode::Key1, KeyCode::Key2, KeyCode::Key3,
KeyCode::Key4, KeyCode::Key5, KeyCode::Key6,
KeyCode::Key7, KeyCode::Key8, KeyCode::Key9,
];
KEYS.get(idx).is_some_and(|&kc| is_key_pressed(kc))
}
/// Did the player press the inventory-letter key for index `idx`
/// (0 = `a`, 1 = `b`, ..., 25 = `z`)? Routes through `is_key_pressed`
/// the same way action lookups do, so the rest of the code never
/// types `KeyCode::A` etc. directly.
pub fn letter_pressed(idx: usize) -> bool {
const KEYS: [KeyCode; 26] = [
KeyCode::A, KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E,
KeyCode::F, KeyCode::G, KeyCode::H, KeyCode::I, KeyCode::J,
KeyCode::K, KeyCode::L, KeyCode::M, KeyCode::N, KeyCode::O,
KeyCode::P, KeyCode::Q, KeyCode::R, KeyCode::S, KeyCode::T,
KeyCode::U, KeyCode::V, KeyCode::W, KeyCode::X, KeyCode::Y,
KeyCode::Z,
];
KEYS.get(idx).is_some_and(|&kc| is_key_pressed(kc))
}
/// Resolve a one-shot cardinal direction press from any of the
/// movement actions. Returns the unit vector for the first action
/// whose key was pressed this frame, or `None` if no direction key
/// fired. Used by the multi-direction interact prompt — the player
/// taps a direction, we resolve it once, and dismiss.
pub fn direction_pressed(bindings: &Bindings) -> Option<(i32, i32)> {
if bindings.pressed(Action::Up) { return Some((0, -1)); }
if bindings.pressed(Action::Down) { return Some((0, 1)); }
if bindings.pressed(Action::Left) { return Some((-1, 0)); }
if bindings.pressed(Action::Right) { return Some(( 1, 0)); }
None
}
/// Like [`direction_pressed`] but also accepts the wait keys
/// (`.` / Space / Kp5) as `(0, 0)` — "this tile". Used by the
/// interact-direction prompt to let the player select a prop they
/// share a tile with (e.g. a crate they're standing on) when
/// adjacent interactables would otherwise win the prompt.
pub fn direction_or_self_pressed(bindings: &Bindings) -> Option<(i32, i32)> {
if let Some(d) = direction_pressed(bindings) { return Some(d); }
if bindings.pressed(Action::Wait) { return Some((0, 0)); }
None
}