Merge branch 'mouse_interaction' into 'master'

Mouse interaction

Closes #40

See merge request ynerant/squirrel-battle!45
This commit is contained in:
ynerant 2020-12-11 18:47:20 +01:00
commit 81de0d8eb0
8 changed files with 114 additions and 10 deletions

View File

@ -5,6 +5,7 @@ import curses
from typing import Any, Optional, Union from typing import Any, Optional, Union
from squirrelbattle.display.texturepack import TexturePack from squirrelbattle.display.texturepack import TexturePack
from squirrelbattle.game import Game
from squirrelbattle.tests.screen import FakePad from squirrelbattle.tests.screen import FakePad
@ -86,6 +87,13 @@ class Display:
def display(self) -> None: def display(self) -> None:
raise NotImplementedError raise NotImplementedError
def handle_click(self, y: int, x: int, game: Game) -> None:
"""
A mouse click was performed on the coordinates (y, x) of the pad.
Maybe it can do something.
"""
pass
@property @property
def rows(self) -> int: def rows(self) -> int:
return curses.LINES if self.screen else 42 return curses.LINES if self.screen else 42

View File

@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit from squirrelbattle.display.display import VerticalSplit, HorizontalSplit, \
Display
from squirrelbattle.display.mapdisplay import MapDisplay from squirrelbattle.display.mapdisplay import MapDisplay
from squirrelbattle.display.messagedisplay import MessageDisplay from squirrelbattle.display.messagedisplay import MessageDisplay
from squirrelbattle.display.statsdisplay import StatsDisplay from squirrelbattle.display.statsdisplay import StatsDisplay
@ -10,7 +11,7 @@ from squirrelbattle.display.menudisplay import MainMenuDisplay, \
PlayerInventoryDisplay, StoreInventoryDisplay, SettingsMenuDisplay PlayerInventoryDisplay, StoreInventoryDisplay, SettingsMenuDisplay
from squirrelbattle.display.logsdisplay import LogsDisplay from squirrelbattle.display.logsdisplay import LogsDisplay
from squirrelbattle.display.texturepack import TexturePack from squirrelbattle.display.texturepack import TexturePack
from typing import Any from typing import Any, List
from squirrelbattle.game import Game, GameMode from squirrelbattle.game import Game, GameMode
from squirrelbattle.enums import DisplayActions from squirrelbattle.enums import DisplayActions
@ -39,11 +40,13 @@ class DisplayManager:
self.storeinventorydisplay] self.storeinventorydisplay]
self.update_game_components() self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None: def handle_display_action(self, action: DisplayActions, *params) -> None:
if action == DisplayActions.REFRESH: if action == DisplayActions.REFRESH:
self.refresh() self.refresh()
elif action == DisplayActions.UPDATE: elif action == DisplayActions.UPDATE:
self.update_game_components() self.update_game_components()
elif action == DisplayActions.MOUSE:
self.handle_mouse_click(*params)
def update_game_components(self) -> None: def update_game_components(self) -> None:
for d in self.displays: for d in self.displays:
@ -58,7 +61,21 @@ class DisplayManager:
self.logsdisplay.update_logs(self.game.logs) self.logsdisplay.update_logs(self.game.logs)
self.messagedisplay.update_message(self.game.message) self.messagedisplay.update_message(self.game.message)
def refresh(self) -> None: def handle_mouse_click(self, y: int, x: int) -> None:
displays = self.refresh()
display = None
for d in displays:
top_y, top_x, height, width = d.y, d.x, d.height, d.width
if top_y <= y < top_y + height and top_x <= x < top_x + width:
# The click coordinates correspond to the coordinates
# of that display
display = d
if display:
display.handle_click(y - display.y, x - display.x, self.game)
def refresh(self) -> List[Display]:
displays = []
if self.game.state == GameMode.PLAY \ if self.game.state == GameMode.PLAY \
or self.game.state == GameMode.INVENTORY \ or self.game.state == GameMode.INVENTORY \
or self.game.state == GameMode.STORE: or self.game.state == GameMode.STORE:
@ -74,18 +91,26 @@ class DisplayManager:
self.rows // 5 - 1, self.cols * 4 // 5) self.rows // 5 - 1, self.cols * 4 // 5)
self.hbar.refresh(self.rows * 4 // 5, 0, 1, self.cols * 4 // 5) self.hbar.refresh(self.rows * 4 // 5, 0, 1, self.cols * 4 // 5)
self.vbar.refresh(0, self.cols * 4 // 5, self.rows, 1) self.vbar.refresh(0, self.cols * 4 // 5, self.rows, 1)
displays += [self.mapdisplay, self.statsdisplay, self.logsdisplay,
self.hbar, self.vbar]
if self.game.state == GameMode.INVENTORY: if self.game.state == GameMode.INVENTORY:
self.playerinventorydisplay.refresh( self.playerinventorydisplay.refresh(
self.rows // 10, self.cols // 2, self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5) 8 * self.rows // 10, 2 * self.cols // 5)
displays.append(self.playerinventorydisplay)
elif self.game.state == GameMode.STORE: elif self.game.state == GameMode.STORE:
self.storeinventorydisplay.refresh( self.storeinventorydisplay.refresh(
self.rows // 10, self.cols // 2, self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5) 8 * self.rows // 10, 2 * self.cols // 5)
displays.append(self.storeinventorydisplay)
elif self.game.state == GameMode.MAINMENU: elif self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.mainmenudisplay)
elif self.game.state == GameMode.SETTINGS: elif self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols) self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.settingsmenudisplay)
if self.game.message: if self.game.message:
height, width = 0, 0 height, width = 0, 0
@ -94,9 +119,12 @@ class DisplayManager:
width = max(width, len(line)) width = max(width, len(line))
y, x = (self.rows - height) // 2, (self.cols - width) // 2 y, x = (self.rows - height) // 2, (self.cols - width) // 2
self.messagedisplay.refresh(y, x, height, width) self.messagedisplay.refresh(y, x, height, width)
displays.append(self.messagedisplay)
self.resize_window() self.resize_window()
return displays
def resize_window(self) -> bool: def resize_window(self) -> bool:
""" """
If the window got resized, ensure that the screen size got updated. If the window got resized, ensure that the screen size got updated.

View File

@ -1,10 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from typing import List from typing import List
from squirrelbattle.menus import Menu, MainMenu from squirrelbattle.menus import Menu, MainMenu
from .display import Display, Box from .display import Display, Box
from ..enums import KeyValues
from ..game import Game
from ..resources import ResourceManager from ..resources import ResourceManager
from ..translations import gettext as _ from ..translations import gettext as _
@ -44,6 +47,13 @@ class MenuDisplay(Display):
self.height - 2 + self.y, self.height - 2 + self.y,
self.width - 2 + self.x) self.width - 2 + self.x)
def handle_click(self, y: int, x: int, game: Game) -> None:
"""
We can select a menu item with the mouse.
"""
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 1))
game.handle_key_pressed(KeyValues.ENTER)
@property @property
def truewidth(self) -> int: def truewidth(self) -> int:
return max([len(str(a)) for a in self.values]) return max([len(str(a)) for a in self.values])
@ -108,6 +118,14 @@ class MainMenuDisplay(Display):
menuy, menux, min(self.menudisplay.preferred_height, menuy, menux, min(self.menudisplay.preferred_height,
self.height - menuy), menuwidth) self.height - menuy), menuwidth)
def handle_click(self, y: int, x: int, game: Game) -> None:
menuwidth = min(self.menudisplay.preferred_width, self.width)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
menuheight = min(self.menudisplay.preferred_height, self.height - menuy)
if menuy <= y < menuy + menuheight and menux <= x < menux + menuwidth:
self.menudisplay.handle_click(y - menuy, x - menux, game)
class PlayerInventoryDisplay(MenuDisplay): class PlayerInventoryDisplay(MenuDisplay):
message = _("== INVENTORY ==") message = _("== INVENTORY ==")
@ -129,6 +147,13 @@ class PlayerInventoryDisplay(MenuDisplay):
def trueheight(self) -> int: def trueheight(self) -> int:
return 2 + super().trueheight return 2 + super().trueheight
def handle_click(self, y: int, x: int, game: Game) -> None:
"""
We can select a menu item with the mouse.
"""
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 3))
game.handle_key_pressed(KeyValues.ENTER)
class StoreInventoryDisplay(MenuDisplay): class StoreInventoryDisplay(MenuDisplay):
message = _("== STALL ==") message = _("== STALL ==")
@ -150,3 +175,10 @@ class StoreInventoryDisplay(MenuDisplay):
@property @property
def trueheight(self) -> int: def trueheight(self) -> int:
return 2 + super().trueheight return 2 + super().trueheight
def handle_click(self, y: int, x: int, game: Game) -> None:
"""
We can select a menu item with the mouse.
"""
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 3))
game.handle_key_pressed(KeyValues.ENTER)

View File

@ -31,7 +31,7 @@ class Item(Entity):
""" """
if self.held: if self.held:
self.held_by.inventory.remove(self) self.held_by.inventory.remove(self)
self.map.add_entity(self) self.held_by.map.add_entity(self)
self.move(self.held_by.y, self.held_by.x) self.move(self.held_by.y, self.held_by.x)
self.held = False self.held = False
self.held_by = None self.held_by = None

View File

@ -16,6 +16,7 @@ class DisplayActions(Enum):
""" """
REFRESH = auto() REFRESH = auto()
UPDATE = auto() UPDATE = auto()
MOUSE = auto()
class GameMode(Enum): class GameMode(Enum):
@ -33,6 +34,7 @@ class KeyValues(Enum):
""" """
Key values options used in the game Key values options used in the game
""" """
MOUSE = auto()
UP = auto() UP = auto()
DOWN = auto() DOWN = auto()
LEFT = auto() LEFT = auto()

View File

@ -4,6 +4,7 @@
from json import JSONDecodeError from json import JSONDecodeError
from random import randint from random import randint
from typing import Any, Optional from typing import Any, Optional
import curses
import json import json
import os import os
import sys import sys
@ -15,7 +16,6 @@ from .resources import ResourceManager
from .settings import Settings from .settings import Settings
from . import menus from . import menus
from .translations import gettext as _, Translator from .translations import gettext as _, Translator
from typing import Callable
class Game: class Game:
@ -26,7 +26,7 @@ class Game:
player: Player player: Player
screen: Any screen: Any
# display_actions is a display interface set by the bootstrapper # display_actions is a display interface set by the bootstrapper
display_actions: Callable[[DisplayActions], None] display_actions: callable
def __init__(self) -> None: def __init__(self) -> None:
""" """
@ -71,6 +71,10 @@ class Game:
screen.refresh() screen.refresh()
self.display_actions(DisplayActions.REFRESH) self.display_actions(DisplayActions.REFRESH)
key = screen.getkey() key = screen.getkey()
if key == "KEY_MOUSE":
_ignored1, x, y, _ignored2, _ignored3 = curses.getmouse()
self.display_actions(DisplayActions.MOUSE, y, x)
else:
self.handle_key_pressed( self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key) KeyValues.translate_key(key, self.settings), key)

View File

@ -20,6 +20,8 @@ class TermManager: # pragma: no cover
curses.cbreak() curses.cbreak()
# make cursor invisible # make cursor invisible
curses.curs_set(False) curses.curs_set(False)
# Catch mouse events
curses.mousemask(True)
# Enable colors # Enable colors
curses.start_color() curses.start_color()

View File

@ -230,6 +230,33 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.SPACE) self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU) self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_mouse_click(self) -> None:
"""
Simulate mouse clicks.
"""
self.game.state = GameMode.MAINMENU
# Settings menu
self.game.display_actions(DisplayActions.MOUSE, 25, 21)
self.assertEqual(self.game.main_menu.position, 4)
self.assertEqual(self.game.state, GameMode.SETTINGS)
bomb = Bomb()
bomb.hold(self.game.player)
bomb2 = Bomb()
bomb2.hold(self.game.player)
self.game.state = GameMode.INVENTORY
# Click nowhere
self.game.display_actions(DisplayActions.MOUSE, 0, 0)
self.assertEqual(self.game.state, GameMode.INVENTORY)
# Click on the second item
self.game.display_actions(DisplayActions.MOUSE, 8, 25)
self.assertEqual(self.game.state, GameMode.INVENTORY)
self.assertEqual(self.game.inventory_menu.position, 1)
def test_new_game(self) -> None: def test_new_game(self) -> None:
""" """
Ensure that the start button starts a new game. Ensure that the start button starts a new game.
@ -512,9 +539,10 @@ class TestGame(unittest.TestCase):
# The second item is not a heart # The second item is not a heart
merchant.inventory[1] = Sword() merchant.inventory[1] = Sword()
# Buy the second item # Buy the second item by clicking on it
item = self.game.store_menu.validate() item = self.game.store_menu.validate()
self.assertIn(item, merchant.inventory) self.assertIn(item, merchant.inventory)
self.game.display_actions(DisplayActions.MOUSE, 8, 25)
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
self.assertIn(item, self.game.player.inventory) self.assertIn(item, self.game.player.inventory)
self.assertNotIn(item, merchant.inventory) self.assertNotIn(item, merchant.inventory)