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 squirrelbattle.display.texturepack import TexturePack
from squirrelbattle.game import Game
from squirrelbattle.tests.screen import FakePad
@ -86,6 +87,13 @@ class Display:
def display(self) -> None:
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
def rows(self) -> int:
return curses.LINES if self.screen else 42

View File

@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
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.messagedisplay import MessageDisplay
from squirrelbattle.display.statsdisplay import StatsDisplay
@ -10,7 +11,7 @@ from squirrelbattle.display.menudisplay import MainMenuDisplay, \
PlayerInventoryDisplay, StoreInventoryDisplay, SettingsMenuDisplay
from squirrelbattle.display.logsdisplay import LogsDisplay
from squirrelbattle.display.texturepack import TexturePack
from typing import Any
from typing import Any, List
from squirrelbattle.game import Game, GameMode
from squirrelbattle.enums import DisplayActions
@ -39,11 +40,13 @@ class DisplayManager:
self.storeinventorydisplay]
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:
self.refresh()
elif action == DisplayActions.UPDATE:
self.update_game_components()
elif action == DisplayActions.MOUSE:
self.handle_mouse_click(*params)
def update_game_components(self) -> None:
for d in self.displays:
@ -58,7 +61,21 @@ class DisplayManager:
self.logsdisplay.update_logs(self.game.logs)
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 \
or self.game.state == GameMode.INVENTORY \
or self.game.state == GameMode.STORE:
@ -74,18 +91,26 @@ class DisplayManager:
self.rows // 5 - 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)
displays += [self.mapdisplay, self.statsdisplay, self.logsdisplay,
self.hbar, self.vbar]
if self.game.state == GameMode.INVENTORY:
self.playerinventorydisplay.refresh(
self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5)
displays.append(self.playerinventorydisplay)
elif self.game.state == GameMode.STORE:
self.storeinventorydisplay.refresh(
self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5)
displays.append(self.storeinventorydisplay)
elif self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.mainmenudisplay)
elif self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.settingsmenudisplay)
if self.game.message:
height, width = 0, 0
@ -94,9 +119,12 @@ class DisplayManager:
width = max(width, len(line))
y, x = (self.rows - height) // 2, (self.cols - width) // 2
self.messagedisplay.refresh(y, x, height, width)
displays.append(self.messagedisplay)
self.resize_window()
return displays
def resize_window(self) -> bool:
"""
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
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import List
from squirrelbattle.menus import Menu, MainMenu
from .display import Display, Box
from ..enums import KeyValues
from ..game import Game
from ..resources import ResourceManager
from ..translations import gettext as _
@ -44,6 +47,13 @@ class MenuDisplay(Display):
self.height - 2 + self.y,
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
def truewidth(self) -> int:
return max([len(str(a)) for a in self.values])
@ -108,6 +118,14 @@ class MainMenuDisplay(Display):
menuy, menux, min(self.menudisplay.preferred_height,
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):
message = _("== INVENTORY ==")
@ -129,6 +147,13 @@ class PlayerInventoryDisplay(MenuDisplay):
def trueheight(self) -> int:
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):
message = _("== STALL ==")
@ -150,3 +175,10 @@ class StoreInventoryDisplay(MenuDisplay):
@property
def trueheight(self) -> int:
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:
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.held = False
self.held_by = None

View File

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

View File

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

View File

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

View File

@ -230,6 +230,33 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.SPACE)
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:
"""
Ensure that the start button starts a new game.
@ -512,9 +539,10 @@ class TestGame(unittest.TestCase):
# The second item is not a heart
merchant.inventory[1] = Sword()
# Buy the second item
# Buy the second item by clicking on it
item = self.game.store_menu.validate()
self.assertIn(item, merchant.inventory)
self.game.display_actions(DisplayActions.MOUSE, 8, 25)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertIn(item, self.game.player.inventory)
self.assertNotIn(item, merchant.inventory)