From bbe37eab97df7d985b1561ea4d59a7b220f5bbbd Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 16:56:22 +0100 Subject: [PATCH 1/7] Listen for clicks, detect which display was clicked --- squirrelbattle/display/display_manager.py | 34 ++++++++++++++++++++--- squirrelbattle/enums.py | 8 ++++-- squirrelbattle/game.py | 9 ++++-- squirrelbattle/term_manager.py | 2 ++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index 0e9cf04..a9429fd 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -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, \ InventoryDisplay, 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 @@ -36,11 +37,13 @@ class DisplayManager: self.logsdisplay, self.messagedisplay] 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: @@ -52,7 +55,20 @@ 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 + raise Exception(f"click at ({y}, {x})", display) + + def refresh(self) -> List[Display]: + displays = [] + if self.game.state == GameMode.PLAY \ or self.game.state == GameMode.INVENTORY: # The map pad has already the good size @@ -67,15 +83,22 @@ 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.inventorydisplay.refresh(self.rows // 10, self.cols // 2, 8 * self.rows // 10, 2 * self.cols // 5) + displays.append(self.inventorydisplay) 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 @@ -84,9 +107,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. diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index 84eb498..5784568 100644 --- a/squirrelbattle/enums.py +++ b/squirrelbattle/enums.py @@ -16,6 +16,7 @@ class DisplayActions(Enum): """ REFRESH = auto() UPDATE = auto() + MOUSE = auto() class GameMode(Enum): @@ -32,6 +33,7 @@ class KeyValues(Enum): """ Key values options used in the game """ + MOUSE = auto() UP = auto() DOWN = auto() LEFT = auto() @@ -48,8 +50,10 @@ class KeyValues(Enum): """ Translate the raw string key into an enum value that we can use. """ - if key in (settings.KEY_DOWN_SECONDARY, - settings.KEY_DOWN_PRIMARY): + if key == "KEY_MOUSE": + return KeyValues.MOUSE + elif key in (settings.KEY_DOWN_SECONDARY, + settings.KEY_DOWN_PRIMARY): return KeyValues.DOWN elif key in (settings.KEY_LEFT_PRIMARY, settings.KEY_LEFT_SECONDARY): diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 6d9e9e7..0758e19 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -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: @@ -25,7 +25,7 @@ class Game: map: Map player: Player # display_actions is a display interface set by the bootstrapper - display_actions: Callable[[DisplayActions], None] + display_actions: callable def __init__(self) -> None: """ @@ -82,6 +82,11 @@ class Game: self.display_actions(DisplayActions.REFRESH) return + if key == KeyValues.MOUSE: + _ignored1, x, y, _ignored2, _ignored3 = curses.getmouse() + self.display_actions(DisplayActions.MOUSE, y, x) + return + if self.state == GameMode.PLAY: self.handle_key_pressed_play(key) elif self.state == GameMode.INVENTORY: diff --git a/squirrelbattle/term_manager.py b/squirrelbattle/term_manager.py index 6284173..5a98a4a 100644 --- a/squirrelbattle/term_manager.py +++ b/squirrelbattle/term_manager.py @@ -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() From d50f6701f4d3e35e5b6ee4564bd4c697d8797671 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 17:40:56 +0100 Subject: [PATCH 2/7] Open a menu with the mouse --- squirrelbattle/display/display.py | 7 +++++++ squirrelbattle/display/display_manager.py | 3 ++- squirrelbattle/display/menudisplay.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/display/display.py b/squirrelbattle/display/display.py index 9cc1456..ef4053f 100644 --- a/squirrelbattle/display/display.py +++ b/squirrelbattle/display/display.py @@ -86,6 +86,13 @@ class Display: def display(self) -> None: raise NotImplementedError + def handle_click(self, y: int, x: int) -> 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 diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index a9429fd..708f003 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -64,7 +64,8 @@ class DisplayManager: # The click coordinates correspond to the coordinates # of that display display = d - raise Exception(f"click at ({y}, {x})", display) + if display: + display.handle_click(y - display.y, x - display.x) def refresh(self) -> List[Display]: displays = [] diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index d040d81..a494484 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -41,6 +41,13 @@ class MenuDisplay(Display): self.height - 2 + self.y, self.width - 2 + self.x) + def handle_click(self, y: int, x: int) -> None: + """ + We can select a menu item with the mouse. + """ + self.menu.position = y - 1 + curses.ungetch('\n') + @property def truewidth(self) -> int: return max([len(str(a)) for a in self.values]) @@ -99,6 +106,14 @@ class MainMenuDisplay(Display): menuy, menux, min(self.menudisplay.preferred_height, self.height - menuy), menuwidth) + def handle_click(self, y: int, x: int) -> 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) + class InventoryDisplay(MenuDisplay): def update_pad(self) -> None: From 089a247b2fdf9c56a45600195fe581b5472bb040 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 17:43:46 +0100 Subject: [PATCH 3/7] Maybe mouse clicks may use the game --- squirrelbattle/display/display.py | 3 ++- squirrelbattle/display/display_manager.py | 2 +- squirrelbattle/display/menudisplay.py | 11 +++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/squirrelbattle/display/display.py b/squirrelbattle/display/display.py index ef4053f..c5b1aa3 100644 --- a/squirrelbattle/display/display.py +++ b/squirrelbattle/display/display.py @@ -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,7 +87,7 @@ class Display: def display(self) -> None: raise NotImplementedError - def handle_click(self, y: int, x: int) -> None: + 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. diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index 708f003..743baef 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -65,7 +65,7 @@ class DisplayManager: # of that display display = d if display: - display.handle_click(y - display.y, x - display.x) + display.handle_click(y - display.y, x - display.x, self.game) def refresh(self) -> List[Display]: displays = [] diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index a494484..d745c28 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -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 _ @@ -41,12 +44,12 @@ class MenuDisplay(Display): self.height - 2 + self.y, self.width - 2 + self.x) - def handle_click(self, y: int, x: int) -> None: + def handle_click(self, y: int, x: int, game: Game) -> None: """ We can select a menu item with the mouse. """ self.menu.position = y - 1 - curses.ungetch('\n') + game.handle_key_pressed(KeyValues.ENTER) @property def truewidth(self) -> int: @@ -106,13 +109,13 @@ class MainMenuDisplay(Display): menuy, menux, min(self.menudisplay.preferred_height, self.height - menuy), menuwidth) - def handle_click(self, y: int, x: int) -> None: + 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) + self.menudisplay.handle_click(y - menuy, x - menux, game) class InventoryDisplay(MenuDisplay): From 1afa397fec73b94d7cfb0169ed3e1b6d90060ea0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 18:07:24 +0100 Subject: [PATCH 4/7] Better interaction with inventory menu --- squirrelbattle/display/menudisplay.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index d745c28..eee4606 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -48,7 +48,7 @@ class MenuDisplay(Display): """ We can select a menu item with the mouse. """ - self.menu.position = y - 1 + self.menu.position = max(0, min(len(self.menu.values) - 1, y - 1)) game.handle_key_pressed(KeyValues.ENTER) @property @@ -136,3 +136,10 @@ class InventoryDisplay(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) From d9912cacad5ec6e9f9430124aaf1e4b9a8eb5f87 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 18:17:59 +0100 Subject: [PATCH 5/7] Listen to mouse clicks in the main loop --- squirrelbattle/enums.py | 6 ++---- squirrelbattle/game.py | 13 ++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index 5784568..11f5c17 100644 --- a/squirrelbattle/enums.py +++ b/squirrelbattle/enums.py @@ -50,10 +50,8 @@ class KeyValues(Enum): """ Translate the raw string key into an enum value that we can use. """ - if key == "KEY_MOUSE": - return KeyValues.MOUSE - elif key in (settings.KEY_DOWN_SECONDARY, - settings.KEY_DOWN_PRIMARY): + if key in (settings.KEY_DOWN_SECONDARY, + settings.KEY_DOWN_PRIMARY): return KeyValues.DOWN elif key in (settings.KEY_LEFT_PRIMARY, settings.KEY_LEFT_SECONDARY): diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 0758e19..a37f1d2 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -68,8 +68,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: @@ -82,11 +86,6 @@ class Game: self.display_actions(DisplayActions.REFRESH) return - if key == KeyValues.MOUSE: - _ignored1, x, y, _ignored2, _ignored3 = curses.getmouse() - self.display_actions(DisplayActions.MOUSE, y, x) - return - if self.state == GameMode.PLAY: self.handle_key_pressed_play(key) elif self.state == GameMode.INVENTORY: From f453b82a582efd2a9328f1d005a6d3af772707cb Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 18:33:47 +0100 Subject: [PATCH 6/7] Test clicking on the screen --- squirrelbattle/entities/items.py | 4 ++-- squirrelbattle/interfaces.py | 3 ++- squirrelbattle/tests/game_test.py | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index e90ec32..a1f3bd4 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -28,7 +28,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 @@ -49,7 +49,7 @@ class Item(Entity): """ self.held = True self.held_by = player - self.map.remove_entity(self) + self.held_by.map.remove_entity(self) player.inventory.append(self) def save_state(self) -> dict: diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 3567ea0..92b4498 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -68,7 +68,8 @@ class Map: """ Unregister an entity from the map. """ - self.entities.remove(entity) + if entity in self.entities: + self.entities.remove(entity) def find_entities(self, entity_class: type) -> list: return [entity for entity in self.entities diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 3a32c95..2bb84c5 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -216,6 +216,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. From a4a10e340da74ead2220a00f5bbf8be73266c8d5 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 18:44:05 +0100 Subject: [PATCH 7/7] Test clicking on the merchant pad --- squirrelbattle/display/display_manager.py | 2 +- squirrelbattle/tests/game_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index 203fc82..f9b3f01 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -99,7 +99,7 @@ class DisplayManager: self.playerinventorydisplay.refresh( self.rows // 10, self.cols // 2, 8 * self.rows // 10, 2 * self.cols // 5) - displays.append(self.inventorydisplay) + displays.append(self.playerinventorydisplay) elif self.game.state == GameMode.STORE: self.storeinventorydisplay.refresh( self.rows // 10, self.cols // 2, diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index e079035..bc3ce12 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -539,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)