Merge branch 'mouse_interaction' into 'master'
Mouse interaction Closes #40 See merge request ynerant/squirrel-battle!45
This commit was merged in pull request #126.
	This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user