Source code for nqp.core.input
from __future__ import annotations
import dataclasses
import logging
import time
from typing import Mapping, Tuple, TYPE_CHECKING
import pygame
from pygame.locals import *
from .constants import GamepadAxes, GamepadButton
from .debug import Timer
if TYPE_CHECKING:
    from nqp.core.game import Game
__all__ = ["Input"]
[docs]@dataclasses.dataclass
class ControllerConfig:
    deadzone: float
    axes: Mapping[int, GamepadAxes]
    hat: Mapping[int, Tuple[GamepadButton, GamepadButton]]
    buttons: Mapping[int, GamepadButton]
# specific to xbox gamepad layout, should not be changed by player
# add more configs for other gamepad types: ps3,4,5, switch, etc
xbox_gamepad_config = ControllerConfig(
    deadzone=0.25,
    axes={
        0: GamepadAxes.LEFT_X,
        1: GamepadAxes.LEFT_Y,
        # 2: None,  # l. shoulder
        3: GamepadAxes.RIGHT_X,
        4: GamepadAxes.RIGHT_Y,
        # 5: None,  # r. shoulder
    },
    hat={
        0: (GamepadButton.LEFT, GamepadButton.RIGHT),
        1: (GamepadButton.DOWN, GamepadButton.UP),
    },
    buttons={
        0: GamepadButton.SOUTH,
        1: GamepadButton.EAST,
        2: GamepadButton.WEST,
        3: GamepadButton.NORTH,
        4: GamepadButton.LEFT_SHOULDER_1,
        5: GamepadButton.RIGHT_SHOULDER_1,
        6: GamepadButton.SELECT_OR_BACK,
        7: GamepadButton.START,
        8: GamepadButton.LEFT_STICK,
        9: GamepadButton.RIGHT_STICK,
    },
)
# generic mapping for gamepad buttons to game input labels
# if the player wants to change config, this would need to be changed
gamepad_label_map = {
    GamepadButton.UP: "up",
    GamepadButton.LEFT: "left",
    GamepadButton.DOWN: "down",
    GamepadButton.RIGHT: "right",
    GamepadButton.EAST: "cancel",
    GamepadButton.SOUTH: "select",
    GamepadButton.NORTH: "view_troupe",
}
# map analog axis to digital buttons
gamepad_axis_direction_map = {
    GamepadAxes.LEFT_X: [GamepadButton.LEFT, GamepadButton.RIGHT],
    GamepadAxes.LEFT_Y: [GamepadButton.UP, GamepadButton.DOWN],
}
[docs]class Input:
[docs]    def __init__(self, game: Game):
        with Timer("Input: initialised"):
            self._game: Game = game
            self.states = {
                "right": False,
                "left": False,
                "up": False,
                "down": False,
                "hold_right": False,
                "hold_left": False,
                "hold_up": False,
                "hold_down": False,
                "select": False,
                "cancel": False,
                "view_troupe": False,
                "shift": False,
                "ctrl": False,
                "backspace": False,
                "toggle_dev_console": False,
                "typing_enter": False,
                "tab": False,
                "speed_slow": False,
                "speed_normal": False,
                "speed_fast": False,
                "speed_fastest": False,
            }
            self.mouse_state = {"left": False}
            self.mode = "default"
            self.char_buffer = ""
            self.backspace_hold = 0
            self.mouse_moved = False
            self.mouse_pos: pygame.Vector2 = pygame.Vector2(0, 0)
            self.mouse_pos_raw: pygame.Vector2 = self.mouse_pos
            self._old_mouse: pygame.Vector2 = self.mouse_pos_raw
            self._gamepads = dict()
            self._scan_gamepads()
[docs]    def soft_reset(self):
        """
        Resets inputs that should only be activated for 1 frame.
        """
        self.states["select"] = False
        self.states["cancel"] = False
        self.mouse_state["left"] = False
[docs]    def reset(self):
        """
        Set all input to false.
        """
        for key in self.states.keys():
            self.states[key] = False
[docs]    def update(self, delta_time: float):
        self.soft_reset()
        self.mouse_pos = pygame.Vector2(pygame.mouse.get_pos())
        self.mouse_pos_raw = self.mouse_pos
        self.mouse_pos[0] *= self._game.window.base_resolution[0] / self._game.window.scaled_resolution[0]
        self.mouse_pos[1] *= self._game.window.base_resolution[1] / self._game.window.scaled_resolution[1]
        self.mouse_moved = self.mouse_pos_raw != self._old_mouse
        self._old_mouse = self.mouse_pos_raw
        chars = [
            ".",
            "-",
            "_",
            "a",
            "b",
            "c",
            "d",
            "e",
            "f",
            "g",
            "h",
            "i",
            "j",
            "k",
            "l",
            "m",
            "n",
            "o",
            "p",
            "q",
            "r",
            "s",
            "t",
            "u",
            "v",
            "w",
            "x",
            "y",
            "z",
            "0",
            "1",
            "2",
            "3",
            "4",
            "5",
            "6",
            "7",
            "8",
            "9",
        ]
        if self.mode == "typing":
            if self.states["backspace"]:
                self.backspace_hold += self._game.window.delta_time
                if self.backspace_hold > 0.7:
                    self.backspace_hold -= 0.035
                    self.char_buffer.append("backspace")
            else:
                self.backspace_hold = 0
        for event in pygame.event.get():
            if event.type == QUIT:
                self._game.quit()
            if event.type == MOUSEBUTTONDOWN:
                if event.button == 1:
                    self.mouse_state["left"] = True
            if event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    self._game.quit()
                if event.key in [K_LSHIFT, K_RSHIFT]:
                    self.states["shift"] = True
                if event.key in [K_LCTRL, K_RCTRL]:
                    self.states["ctrl"] = True
                if event.key == K_BACKSPACE:
                    self.states["backspace"] = True
                if event.key == K_BACKQUOTE:
                    self.states["toggle_dev_console"] = True
                if event.key == K_TAB:
                    self.states["tab"] = True
                if self.mode == "typing":
                    if event.key == K_RETURN:
                        self.states["typing_enter"] = True
            if event.type == KEYUP:
                if event.key in [K_LSHIFT, K_RSHIFT]:
                    self.states["shift"] = False
                if event.key == K_BACKSPACE:
                    self.states["backspace"] = False
            # input outside of typing
            if self.mode == "default":
                if event.type == KEYDOWN:
                    if event.key == K_x:
                        self.states["cancel"] = True
                    if event.key == K_RIGHT:
                        self.states["right"] = True
                        self.states["hold_right"] = True
                    if event.key == K_LEFT:
                        self.states["left"] = True
                        self.states["hold_left"] = True
                    if event.key == K_UP:
                        self.states["up"] = True
                        self.states["hold_up"] = True
                    if event.key == K_DOWN:
                        self.states["down"] = True
                        self.states["hold_down"] = True
                    if event.key == K_RETURN:
                        self.states["select"] = True
                    if event.key == K_v:
                        self.states["view_troupe"] = True
                    if event.key == K_F1:
                        self.states["speed_slow"] = True
                    if event.key == K_F2:
                        self.states["speed_normal"] = True
                    if event.key == K_F3:
                        self.states["speed_fast"] = True
                    if event.key == K_F4:
                        self.states["speed_fastest"] = True
                if event.type == KEYUP:
                    if event.key == K_RIGHT:
                        self.states["hold_right"] = False
                    if event.key == K_LEFT:
                        self.states["hold_left"] = False
                    if event.key == K_UP:
                        self.states["hold_up"] = False
                    if event.key == K_DOWN:
                        self.states["hold_down"] = False
                if event.type == JOYBUTTONDOWN:
                    button = xbox_gamepad_config.buttons[event.button]
                    label = gamepad_label_map.get(button)
                    if label:
                        self.states[label] = True
                        self.states["hold_" + label] = True
                if event.type == JOYBUTTONUP:
                    button = xbox_gamepad_config.buttons[event.button]
                    label = gamepad_label_map.get(button)
                    if label:
                        self.states["hold_" + label] = False
                if event.type == JOYHATMOTION:
                    for axis, value in enumerate(event.value):
                        endstops = xbox_gamepad_config.hat[axis]
                        button = None
                        if value == -1:
                            button = endstops[0]
                        elif value == 1:
                            button = endstops[1]
                        if button:
                            label = gamepad_label_map[button]
                            self.states[label] = True
                            self.states["hold_" + label] = True
                        else:
                            for button in endstops:
                                label = gamepad_label_map[button]
                                self.states["hold_" + label] = False
                if event.type == JOYAXISMOTION:
                    axis = xbox_gamepad_config.axes[event.axis]
                    endstops = gamepad_axis_direction_map.get(axis)
                    if endstops is not None:
                        if abs(event.value) >= xbox_gamepad_config.deadzone:
                            button = None
                            if event.value < 0:
                                button = endstops[0]
                            elif event.value > 0:
                                button = endstops[1]
                            if button:
                                label = gamepad_label_map[button]
                                hold_label = "hold_" + label
                                if not self.states[hold_label]:
                                    self.states[label] = True
                                self.states[hold_label] = True
                        else:
                            for button in endstops:
                                label = gamepad_label_map[button]
                                self.states["hold_" + label] = False
            elif self.mode == "typing":
                if event.type == KEYDOWN:
                    for char in chars:
                        if event.key == ord(char):
                            if self.states["shift"]:
                                char = char.upper()
                                if char == "-":
                                    char = "_"
                            self.char_buffer.append(char)
                    if event.key == K_BACKSPACE:
                        self.char_buffer.append("backspace")
                    if event.key == K_SPACE:
                        self.char_buffer.append(" ")
    def _scan_gamepads(self):
        for index in range(pygame.joystick.get_count()):
            gamepad = pygame.joystick.Joystick(index)
            gamepad.init()
            # if the Joystick object is deleted, then event loop
            # will not have joystick events, so we need to keep a ref.
            self._gamepads[gamepad.get_guid()] = gamepad