209 lines
8.6 KiB
Python
209 lines
8.6 KiB
Python
from __future__ import annotations
|
|
|
|
from engine.config import DEFAULT_KEYBINDINGS, KeyBindings
|
|
from engine.gamepad import AXIS_LEFT_X, AXIS_LEFT_Y, GamepadBindings, GamepadState, apply_deadzone
|
|
|
|
MOVE_ACTION_VECTORS: dict[str, tuple[int, int, int]] = {
|
|
"move_up": (0, -1, 0),
|
|
"move_down": (0, 1, 0),
|
|
"move_left": (-1, 0, 0),
|
|
"move_right": (1, 0, 0),
|
|
}
|
|
|
|
# Mouse-button-driven "which hand acts" scheme: left/right click for the primary pair of
|
|
# hands, shift+left/right for a second pair (only meaningful for a 4-armed species; wield_item
|
|
# already refuses to use a hand slot with no corresponding arm, so this degrades safely).
|
|
HAND_ACTIVATION_ACTIONS: dict[str, str] = {
|
|
"activate_right_hand": "right_hand",
|
|
"activate_left_hand": "left_hand",
|
|
"activate_right_hand_2": "right_hand_2",
|
|
"activate_left_hand_2": "left_hand_2",
|
|
}
|
|
|
|
# jupyter_rfb/rendercanvas pointer button numbering (DOM-style): 0 = left, 2 = right.
|
|
MOUSE_BUTTON_KEYS: dict[int, str] = {0: "mouse_left", 2: "mouse_right"}
|
|
|
|
# Numkey hotkeys for the ability bar (engine/ui.py::AbilityBar) - "0" conventionally maps to
|
|
# the 10th slot (index 9), matching the usual 1-9,0 hotbar ordering.
|
|
SELECT_ABILITY_ACTIONS: dict[str, int] = {
|
|
"select_ability_1": 0,
|
|
"select_ability_2": 1,
|
|
"select_ability_3": 2,
|
|
"select_ability_4": 3,
|
|
"select_ability_5": 4,
|
|
"select_ability_6": 5,
|
|
"select_ability_7": 6,
|
|
"select_ability_8": 7,
|
|
"select_ability_9": 8,
|
|
"select_ability_0": 9,
|
|
}
|
|
|
|
DEFAULT_FACING: tuple[int, int, int] = (0, 1, 0)
|
|
|
|
|
|
class InputState:
|
|
"""Discrete key/mouse-driven movement and actions (one press = one step or state toggle),
|
|
resolved through a KeyBindings config so any action can be bound to multiple inputs. Also
|
|
tracks raw pointer position for continuous mouse aiming (see engine/aim.py).
|
|
"""
|
|
|
|
def __init__(self, keybindings: KeyBindings | None = None):
|
|
self.keybindings = keybindings or KeyBindings(dict(DEFAULT_KEYBINDINGS))
|
|
self.pending_move: tuple[int, int, int] | None = None
|
|
self.held_move_actions: set[str] = set()
|
|
self.pending_leap: tuple[int, int, int] | None = None
|
|
self.pending_swim_down = False
|
|
self.pending_hand_activation: str | None = None
|
|
self.pending_ability_select: int | None = None
|
|
self.pending_activate_ability_bar = False
|
|
self.is_blocking = False
|
|
self.facing: tuple[int, int, int] = DEFAULT_FACING
|
|
self.pointer_pos: tuple[float, float] | None = None
|
|
self.gamepad_move_vector: tuple[float, float] = (0.0, 0.0)
|
|
self._prev_gamepad_buttons: set[str] = set()
|
|
|
|
def handle_event(self, event: dict) -> None:
|
|
event_type = event.get("event_type")
|
|
|
|
if event_type == "pointer_move":
|
|
self.pointer_pos = (event.get("x", 0.0), event.get("y", 0.0))
|
|
return
|
|
|
|
if event_type == "pointer_down":
|
|
self._dispatch_down(MOUSE_BUTTON_KEYS.get(event.get("button", -1)), event.get("modifiers", ()))
|
|
return
|
|
|
|
if event_type not in ("key_down", "key_up"):
|
|
return
|
|
|
|
key = event.get("key", "")
|
|
action = self._resolve_action(key, event.get("modifiers", ()))
|
|
if action is None:
|
|
return
|
|
if event_type == "key_down":
|
|
self._apply_down(action)
|
|
else:
|
|
self._apply_up(action)
|
|
|
|
def _resolve_action(self, key: str, modifiers) -> str | None:
|
|
if key and "Shift" in modifiers:
|
|
action = self.keybindings.action_for_key(f"shift+{key}")
|
|
if action is not None:
|
|
return action
|
|
return self.keybindings.action_for_key(key)
|
|
|
|
def _dispatch_down(self, mouse_key: str | None, modifiers) -> None:
|
|
if mouse_key is None:
|
|
return
|
|
action = self._resolve_action(mouse_key, modifiers)
|
|
if action is not None:
|
|
self._apply_down(action)
|
|
|
|
def _apply_down(self, action: str) -> None:
|
|
if action in MOVE_ACTION_VECTORS:
|
|
move = MOVE_ACTION_VECTORS[action]
|
|
self.pending_move = move # one-shot compat, e.g. for a discrete-step caller
|
|
self.held_move_actions.add(action)
|
|
self.facing = move
|
|
elif action == "leap":
|
|
self.pending_leap = self.facing
|
|
elif action == "swim_down":
|
|
self.pending_swim_down = True
|
|
elif action == "block":
|
|
self.is_blocking = True
|
|
elif action in HAND_ACTIVATION_ACTIONS:
|
|
self.pending_hand_activation = HAND_ACTIVATION_ACTIONS[action]
|
|
elif action in SELECT_ABILITY_ACTIONS:
|
|
self.pending_ability_select = SELECT_ABILITY_ACTIONS[action]
|
|
elif action == "activate_selected_ability":
|
|
self.pending_activate_ability_bar = True
|
|
|
|
def _apply_up(self, action: str) -> None:
|
|
if action in MOVE_ACTION_VECTORS:
|
|
self.held_move_actions.discard(action)
|
|
elif action == "block":
|
|
self.is_blocking = False
|
|
|
|
def apply_gamepad_state(self, state: GamepadState, bindings: GamepadBindings) -> None:
|
|
"""Feeds one frame's polled gamepad snapshot through the same action dispatch as
|
|
keyboard/mouse events. Unlike glfw's edge-triggered key_down/key_up callbacks, a
|
|
gamepad is polled every frame, so button transitions have to be detected here by
|
|
diffing against the previous frame's pressed set.
|
|
"""
|
|
pressed = state.digital_buttons()
|
|
for button in pressed - self._prev_gamepad_buttons:
|
|
action = bindings.action_for_button(button)
|
|
if action is not None:
|
|
self._apply_down(action)
|
|
for button in self._prev_gamepad_buttons - pressed:
|
|
action = bindings.action_for_button(button)
|
|
if action is not None:
|
|
self._apply_up(action)
|
|
self._prev_gamepad_buttons = pressed
|
|
|
|
self.gamepad_move_vector = (
|
|
apply_deadzone(state.axis(AXIS_LEFT_X)),
|
|
apply_deadzone(state.axis(AXIS_LEFT_Y)),
|
|
)
|
|
|
|
def consume_move(self) -> tuple[int, int, int] | None:
|
|
move, self.pending_move = self.pending_move, None
|
|
return move
|
|
|
|
def current_move_vector(self) -> tuple[float, float, float]:
|
|
"""The combined direction of all currently-held movement keys plus the left stick's
|
|
deadzoned analog offset (for continuous movement) - the two sources simply add, so
|
|
keyboard and gamepad work either standalone or together.
|
|
|
|
Holding both an up/down and left/right key gives a diagonal vector - not normalized
|
|
here (World.try_move_continuous normalizes it), so callers get the raw combination.
|
|
"""
|
|
dx = dy = 0.0
|
|
for action in self.held_move_actions:
|
|
vx, vy, _ = MOVE_ACTION_VECTORS[action]
|
|
dx += vx
|
|
dy += vy
|
|
dx += self.gamepad_move_vector[0]
|
|
dy += self.gamepad_move_vector[1]
|
|
return (dx, dy, 0.0)
|
|
|
|
def consume_leap(self) -> tuple[int, int, int] | None:
|
|
leap, self.pending_leap = self.pending_leap, None
|
|
return leap
|
|
|
|
def consume_swim_down(self) -> bool:
|
|
fired, self.pending_swim_down = self.pending_swim_down, False
|
|
return fired
|
|
|
|
def consume_hand_activation(self) -> str | None:
|
|
hand, self.pending_hand_activation = self.pending_hand_activation, None
|
|
return hand
|
|
|
|
def consume_ability_select(self) -> int | None:
|
|
index, self.pending_ability_select = self.pending_ability_select, None
|
|
return index
|
|
|
|
def consume_activate_ability_bar(self) -> bool:
|
|
fired, self.pending_activate_ability_bar = self.pending_activate_ability_bar, False
|
|
return fired
|
|
|
|
def clear_held_state(self) -> None:
|
|
"""Drops all held-key/blocking state - call whenever leaving PLAYING for a menu, since
|
|
key_up events (and gamepad polling - see main.py's poll_gamepad) stop reaching this
|
|
class while a menu owns input, so a key/button released mid-menu would otherwise still
|
|
read as held once gameplay resumes.
|
|
|
|
Also resets the gamepad edge-tracking set: without this, a button held continuously
|
|
across the boundary (e.g. still holding block when a pause menu closes) would never see
|
|
a fresh "down" edge on resume, since apply_gamepad_state's diff would find it already
|
|
"pressed" in both the stale pre-pause snapshot and the current one.
|
|
"""
|
|
self.held_move_actions.clear()
|
|
self.is_blocking = False
|
|
self.pending_move = None
|
|
self.pending_leap = None
|
|
self.pending_swim_down = False
|
|
self.pending_hand_activation = None
|
|
self.pending_activate_ability_bar = False
|
|
self._prev_gamepad_buttons = set()
|