Source code for tuxemon.platform.platform_pygame.events
from __future__ import annotations
from collections import defaultdict
from typing import (
Any,
ClassVar,
Dict,
Generator,
List,
Mapping,
Optional,
Tuple,
TypedDict,
)
import pygame
import pygame as pg
from pygame.rect import Rect
from tuxemon import graphics
from tuxemon.platform.const import buttons, events
from tuxemon.platform.events import (
EventQueueHandler,
InputHandler,
PlayerInput,
)
from tuxemon.session import local_session
from tuxemon.ui.draw import blit_alpha
[docs]class PygameEventQueueHandler(EventQueueHandler):
"""Handle all events from the pygame event queue."""
def __init__(self) -> None:
self._inputs: Dict[int, List[InputHandler[Any]]] = defaultdict(list)
[docs] def add_input(self, player: int, handler: InputHandler[Any]) -> None:
"""
Add an input handler to process.
Parameters:
player: Number of the player the handler belongs to.
handler: Handler whose events will be processed from now on.
"""
self._inputs[player].append(handler)
[docs] def set_input(
self,
player: int,
element: int,
handler: InputHandler[Any],
) -> None:
"""
Sets an input handler to process.
Parameters:
player: Number of the player the handler belongs to.
element: Index to modify
handler: Handler whose events will be processed from now on.
"""
self._inputs[player][element] = handler
[docs] def process_events(self) -> Generator[PlayerInput, None, None]:
for pg_event in pg.event.get():
for inputs in self._inputs.values():
for player_input in inputs:
player_input.process_event(pg_event)
if pg_event.type == pg.QUIT:
local_session.client.event_engine.execute_action("quit")
for inputs in self._inputs.values():
for player_input in inputs:
yield from player_input.get_events()
[docs]class PygameEventHandler(InputHandler[pygame.event.Event]):
"""
Input handler of Pygame events.
"""
[docs]class PygameGamepadInput(PygameEventHandler):
"""
Gamepad event handler.
NOTE: Due to implementation, you may receive "released" inputs for
buttons/directions/axis even if they are released already. Pressed
or held inputs will never be duplicated and are always "correct".
Parameters:
event_map: Mapping of original identifiers to button identifiers.
deadzone: Threshold used to detect when an analog stick should
be considered not pressed, as obtaining an exact value of 0 is
almost impossible.
"""
# Xbox 360 Controller buttons:
# A = 0 Start = 7 D-Up = 13
# B = 1 Back = 6 D-Down = 14
# X = 2 D-Left = 11
# Y = 3 D-Right = 12
#
default_input_map = {
0: buttons.A,
1: buttons.B,
6: buttons.BACK,
11: buttons.LEFT,
12: buttons.RIGHT,
13: buttons.UP,
14: buttons.DOWN,
7: buttons.START,
}
def __init__(
self,
event_map: Optional[Mapping[Optional[int], int]] = None,
deadzone: float = 0.25,
) -> None:
super().__init__(event_map)
self.deadzone = deadzone
[docs] def process_event(self, input_event: pygame.event.Event) -> None:
self.check_button(input_event)
self.check_hat(input_event)
self.check_axis(input_event)
[docs] def check_button(self, pg_event: pygame.event.Event) -> None:
try:
button = self.event_map[pg_event.button]
if pg_event.type == pg.JOYBUTTONDOWN:
self.press(button)
elif pg_event.type == pg.JOYBUTTONUP:
self.release(button)
except (KeyError, AttributeError):
pass
[docs] def check_hat(self, pg_event: pygame.event.Event) -> None:
if pg_event.type == pg.JOYHATMOTION:
x, y = pg_event.value
if x == -1:
self.press(buttons.LEFT, value=x * -1)
elif x == 0:
self.release(buttons.LEFT)
self.release(buttons.RIGHT)
elif x == 1:
self.press(buttons.RIGHT)
if y == -1:
self.press(buttons.DOWN, value=y * -1)
elif y == 0:
self.release(buttons.DOWN)
self.release(buttons.UP)
elif y == 1:
self.press(buttons.UP)
[docs] def check_axis(self, pg_event: pygame.event.Event) -> None:
if pg_event.type == pg.JOYAXISMOTION:
value = pg_event.value
if pg_event.axis == 0:
if abs(value) >= self.deadzone:
if value < 0:
self.press(buttons.LEFT, value * -1)
else:
self.press(buttons.RIGHT, value)
else:
self.release(buttons.LEFT)
self.release(buttons.RIGHT)
elif pg_event.axis == 1:
if abs(value) >= self.deadzone:
if value < 0:
self.press(buttons.UP, value * -1)
else:
self.press(buttons.DOWN, value)
else:
self.release(buttons.UP)
self.release(buttons.DOWN)
[docs]class PygameKeyboardInput(PygameEventHandler):
"""
Keyboard event handler.
Parameters:
event_map: Mapping of original identifiers to button identifiers.
"""
default_input_map = {
pg.K_UP: buttons.UP,
pg.K_DOWN: buttons.DOWN,
pg.K_LEFT: buttons.LEFT,
pg.K_RIGHT: buttons.RIGHT,
pg.K_RETURN: buttons.A,
pg.K_RSHIFT: buttons.B,
pg.K_LSHIFT: buttons.B,
pg.K_ESCAPE: buttons.BACK,
pg.K_BACKSPACE: events.BACKSPACE,
None: events.UNICODE,
}
[docs] def process_event(self, input_event: pygame.event.Event) -> None:
pressed = input_event.type == pg.KEYDOWN
released = input_event.type == pg.KEYUP
if pressed or released:
# try to get game-specific action for the key
try:
button = self.event_map[input_event.key]
except KeyError:
pass
else:
if pressed:
self.press(button)
else:
self.release(button)
return
# just get unicode value
try:
if pressed:
self.release(events.UNICODE)
self.press(events.UNICODE, input_event.unicode)
else:
self.release(events.UNICODE)
except AttributeError:
pass
# TODO: Someone should refactor these to proper classes.
[docs]class DPadInfo(TypedDict):
surface: pygame.surface.Surface
position: Tuple[int, int]
rect: DPadRectsInfo
[docs]class DPadButtonInfo(TypedDict):
surface: pygame.surface.Surface
position: Tuple[int, int]
rect: Rect
[docs]class PygameTouchOverlayInput(PygameEventHandler):
"""
Touch overlay event handler.
Parameters:
transparency: Transparency of the drawn overlay.
"""
default_input_map: ClassVar[Mapping[Optional[int], int]] = {}
def __init__(self, transparency: int) -> None:
super().__init__()
self.transparency = transparency
self.dpad: DPadInfo = {} # type: ignore[typeddict-item]
self.a_button: DPadButtonInfo = {} # type: ignore[typeddict-item]
self.b_button: DPadButtonInfo = {} # type: ignore[typeddict-item]
# TODO: try to simplify this
self.buttons[buttons.UP] = PlayerInput(buttons.UP)
self.buttons[buttons.DOWN] = PlayerInput(buttons.DOWN)
self.buttons[buttons.LEFT] = PlayerInput(buttons.LEFT)
self.buttons[buttons.RIGHT] = PlayerInput(buttons.RIGHT)
self.buttons[buttons.A] = PlayerInput(buttons.A)
self.buttons[buttons.B] = PlayerInput(buttons.B)
[docs] def process_event(self, input_event: pygame.event.Event) -> None:
"""
Process a Pygame event.
Process all events from the controller overlay and pass them down to
current State. All controller overlay events are converted to keyboard
events for compatibility. This is primarily used for the mobile version
of Tuxemon.
Will probably be janky with multi touch.
Parameters:
input_event: Input event to process.
"""
pressed = input_event.type == pg.MOUSEBUTTONDOWN
released = input_event.type == pg.MOUSEBUTTONUP
button = None
if (pressed or released) and input_event.button == 1:
mouse_pos = input_event.pos
dpad_rect = self.dpad["rect"]
if dpad_rect["up"].collidepoint(mouse_pos):
button = buttons.UP
elif dpad_rect["down"].collidepoint(mouse_pos):
button = buttons.DOWN
elif dpad_rect["left"].collidepoint(mouse_pos):
button = buttons.LEFT
elif dpad_rect["right"].collidepoint(mouse_pos):
button = buttons.RIGHT
elif self.a_button["rect"].collidepoint(mouse_pos):
button = buttons.A
elif self.b_button["rect"].collidepoint(mouse_pos):
button = buttons.B
if pressed and button:
self.press(button)
elif released:
for button in self.buttons:
self.release(button)
[docs] def load(self) -> None:
"""Load the touch overlay attributes."""
from tuxemon import prepare
self.dpad["surface"] = graphics.load_and_scale("gfx/d-pad.png")
self.dpad["position"] = (
0,
prepare.SCREEN_SIZE[1] - self.dpad["surface"].get_height(),
)
# Create the collision rectangle objects for the dpad so we can see
# if we're pressing a button
up = Rect(
self.dpad["position"][0] + (self.dpad["surface"].get_width() / 3),
self.dpad["position"][1], # Rectangle position_y
self.dpad["surface"].get_width() / 3, # Rectangle size_x
self.dpad["surface"].get_height() / 2,
)
down = Rect(
self.dpad["position"][0] + (self.dpad["surface"].get_width() / 3),
self.dpad["position"][1] + (self.dpad["surface"].get_height() / 2),
self.dpad["surface"].get_width() / 3,
self.dpad["surface"].get_height() / 2,
)
left = Rect(
self.dpad["position"][0],
self.dpad["position"][1] + (self.dpad["surface"].get_height() / 3),
self.dpad["surface"].get_width() / 2,
self.dpad["surface"].get_height() / 3,
)
right = Rect(
self.dpad["position"][0] + (self.dpad["surface"].get_width() / 2),
self.dpad["position"][1] + (self.dpad["surface"].get_height() / 3),
self.dpad["surface"].get_width() / 2,
self.dpad["surface"].get_height() / 3,
)
self.dpad["rect"] = {
"up": up,
"down": down,
"left": left,
"right": right,
}
# Create the buttons
self.a_button["surface"] = graphics.load_and_scale("gfx/a-button.png")
self.a_button["position"] = (
prepare.SCREEN_SIZE[0]
- int(self.a_button["surface"].get_width() * 1.0),
int(
self.dpad["position"][1]
+ (self.dpad["surface"].get_height() / 2)
- (self.a_button["surface"].get_height() / 2)
),
)
self.a_button["rect"] = Rect(
self.a_button["position"][0],
self.a_button["position"][1],
self.a_button["surface"].get_width(),
self.a_button["surface"].get_height(),
)
self.b_button["surface"] = graphics.load_and_scale("gfx/b-button.png")
self.b_button["position"] = (
prepare.SCREEN_SIZE[0]
- int(self.b_button["surface"].get_width() * 2.1),
int(
self.dpad["position"][1]
+ (self.dpad["surface"].get_height() / 2)
- (self.b_button["surface"].get_height() / 2)
),
)
self.b_button["rect"] = Rect(
self.b_button["position"][0],
self.b_button["position"][1],
self.b_button["surface"].get_width(),
self.b_button["surface"].get_height(),
)
[docs] def draw(self, screen: pygame.surface.Surface) -> None:
"""
Draws the controller overlay to the screen.
Parameters:
screen: Screen surface to draw onto.
"""
blit_alpha(
screen,
self.dpad["surface"],
self.dpad["position"],
self.transparency,
)
blit_alpha(
screen,
self.a_button["surface"],
self.a_button["position"],
self.transparency,
)
blit_alpha(
screen,
self.b_button["surface"],
self.b_button["position"],
self.transparency,
)
[docs]class PygameMouseInput(PygameEventHandler):
"""
Mouse event handler.
Parameters:
event_map: Mapping of original identifiers to button identifiers.
"""
default_input_map = {
pg.MOUSEBUTTONDOWN: buttons.MOUSELEFT,
pg.MOUSEBUTTONUP: buttons.MOUSELEFT,
}
[docs] def process_event(self, pg_event: pygame.event.Event) -> None:
if pg_event.type == pg.MOUSEBUTTONDOWN:
self.press(buttons.MOUSELEFT, pg_event.pos)
elif pg_event.type == pg.MOUSEBUTTONUP:
self.release(buttons.MOUSELEFT)