diff --git a/squirrelbattle/display/display.py b/squirrelbattle/display/display.py index f343230..884c997 100644 --- a/squirrelbattle/display/display.py +++ b/squirrelbattle/display/display.py @@ -172,9 +172,19 @@ class Display: if last_y >= window_y and last_x >= window_x: # Refresh the pad only if coordinates are valid - pad.refresh(top_y, top_x, window_y, window_x, last_y, last_x) + pad.noutrefresh(top_y, top_x, window_y, window_x, last_y, last_x) def display(self) -> None: + """ + Draw the content of the display and refresh pads. + """ + raise NotImplementedError + + def update(self, game: Game) -> None: + """ + The game state was updated. + Indicate what to do with the new state. + """ raise NotImplementedError def handle_click(self, y: int, x: int, game: Game) -> None: diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index d87ed9d..0042615 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -56,19 +56,12 @@ class DisplayManager: def update_game_components(self) -> None: """ - Updates the game components, for example when loading a game. + The game state was updated. + Trigger all displays of these modifications. """ for d in self.displays: d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK) - self.mapdisplay.update_map(self.game.map) - self.statsdisplay.update_player(self.game.player) - self.game.inventory_menu.update_player(self.game.player) - self.game.store_menu.update_merchant(self.game.player) - self.playerinventorydisplay.update_menu(self.game.inventory_menu) - self.storeinventorydisplay.update_menu(self.game.store_menu) - self.settingsmenudisplay.update_menu(self.game.settings_menu) - self.logsdisplay.update_logs(self.game.logs) - self.messagedisplay.update_message(self.game.message) + d.update(self.game) def handle_mouse_click(self, y: int, x: int) -> None: """ @@ -90,6 +83,7 @@ class DisplayManager: Refreshes all components on the screen. """ displays = [] + pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK) if self.game.state == GameMode.PLAY \ or self.game.state == GameMode.INVENTORY \ @@ -112,14 +106,24 @@ class DisplayManager: if self.game.state == GameMode.INVENTORY: self.playerinventorydisplay.refresh( - self.rows // 10, self.cols // 2, - 8 * self.rows // 10, 2 * self.cols // 5) + self.rows // 10, + pack.tile_width * (self.cols // (2 * pack.tile_width)), + 8 * self.rows // 10, + pack.tile_width * (2 * self.cols // (5 * pack.tile_width))) 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) + self.rows // 10, + pack.tile_width * (self.cols // (2 * pack.tile_width)), + 8 * self.rows // 10, + pack.tile_width * (2 * self.cols // (5 * pack.tile_width))) + self.playerinventorydisplay.refresh( + self.rows // 10, + pack.tile_width * (self.cols // (10 * pack.tile_width)), + 8 * self.rows // 10, + pack.tile_width * (2 * self.cols // (5 * pack.tile_width))) displays.append(self.storeinventorydisplay) + displays.append(self.playerinventorydisplay) elif self.game.state == GameMode.MAINMENU: self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) displays.append(self.mainmenudisplay) @@ -135,7 +139,8 @@ class DisplayManager: for line in self.game.message.split("\n"): height += 1 width = max(width, len(line)) - y, x = (self.rows - height) // 2, (self.cols - width) // 2 + y = pack.tile_width * (self.rows - height) // (2 * pack.tile_width) + x = pack.tile_width * ((self.cols - width) // (2 * pack.tile_width)) self.messagedisplay.refresh(y, x, height, width) displays.append(self.messagedisplay) diff --git a/squirrelbattle/display/logsdisplay.py b/squirrelbattle/display/logsdisplay.py index 0aac488..434e60b 100644 --- a/squirrelbattle/display/logsdisplay.py +++ b/squirrelbattle/display/logsdisplay.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from squirrelbattle.display.display import Display +from squirrelbattle.game import Game from squirrelbattle.interfaces import Logs @@ -9,12 +10,14 @@ class LogsDisplay(Display): """ A class to handle the display of the logs. """ + + logs: Logs def __init__(self, *args) -> None: super().__init__(*args) self.pad = self.newpad(self.rows, self.cols) - def update_logs(self, logs: Logs) -> None: - self.logs = logs + def update(self, game: Game) -> None: + self.logs = game.logs def display(self) -> None: messages = self.logs.messages[-self.height:] diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index 2b04963..46b85e9 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -3,20 +3,25 @@ from squirrelbattle.interfaces import Map from .display import Display +from ..game import Game class MapDisplay(Display): """ A class to handle the display of the map. """ + + map: Map def __init__(self, *args): super().__init__(*args) - def update_map(self, m: Map) -> None: - self.map = m - self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1) + def update(self, game: Game) -> None: + self.map = game.map + self.pad = self.newpad(self.map.height, + self.pack.tile_width * self.map.width + 1) def update_pad(self) -> None: + self.pad.resize(500, 500) self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack), self.pack.tile_fg_color, self.pack.tile_bg_color) for e in self.map.entities: diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index 84a20ff..cc73010 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -5,8 +5,9 @@ import curses from random import randint from typing import List -from squirrelbattle.menus import Menu, MainMenu +from squirrelbattle.menus import Menu, MainMenu, SettingsMenu, StoreMenu from .display import Box, Display +from ..entities.player import Player from ..enums import KeyValues, GameMode from ..game import Game from ..resources import ResourceManager @@ -17,6 +18,7 @@ class MenuDisplay(Display): """ A class to display the menu objects. """ + menu: Menu position: int def __init__(self, *args, **kwargs): @@ -80,6 +82,11 @@ class SettingsMenuDisplay(MenuDisplay): """ A class to display specifically a settingsmenu object. """ + menu: SettingsMenu + + def update(self, game: Game) -> None: + self.update_menu(game.settings_menu) + @property def values(self) -> List[str]: return [_(a[1][1]) + (" : " @@ -124,6 +131,9 @@ class MainMenuDisplay(Display): menuy, menux, min(self.menudisplay.preferred_height, self.height - menuy), menuwidth) + def update(self, game: Game) -> None: + self.menudisplay.update_menu(game.main_menu) + 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 @@ -143,13 +153,33 @@ class PlayerInventoryDisplay(MenuDisplay): """ A class to handle the display of the player's inventory. """ + player: Player = None + selected: bool = True + store_mode: bool = False + + def update(self, game: Game) -> None: + self.player = game.player + self.update_menu(game.inventory_menu) + self.store_mode = game.state == GameMode.STORE + self.selected = game.state == GameMode.INVENTORY \ + or (self.store_mode and not game.is_in_store_menu) + def update_pad(self) -> None: self.menubox.update_title(_("INVENTORY")) for i, item in enumerate(self.menu.values): rep = self.pack[item.name.upper()] - selection = f"[{rep}]" if i == self.menu.position else f" {rep} " + selection = f"[{rep}]" if i == self.menu.position \ + and self.selected else f" {rep} " self.addstr(self.pad, i + 1, 0, selection - + " " + item.translated_name.capitalize()) + + " " + item.translated_name.capitalize() + + (": " + str(item.price) + " Hazels" + if self.store_mode else "")) + + if self.store_mode: + price = f"{self.pack.HAZELNUT} {self.player.hazel} Hazels" + width = len(price) + (self.pack.tile_width - 1) + self.addstr(self.pad, self.height - 3, self.width - width - 2, + price, italic=True) @property def truewidth(self) -> int: @@ -164,6 +194,7 @@ class PlayerInventoryDisplay(MenuDisplay): We can select a menu item with the mouse. """ self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2)) + game.is_in_store_menu = False game.handle_key_pressed(KeyValues.ENTER) @@ -171,15 +202,28 @@ class StoreInventoryDisplay(MenuDisplay): """ A class to handle the display of a merchant's inventory. """ + menu: StoreMenu + selected: bool = False + + def update(self, game: Game) -> None: + self.update_menu(game.store_menu) + self.selected = game.is_in_store_menu + def update_pad(self) -> None: self.menubox.update_title(_("STALL")) for i, item in enumerate(self.menu.values): rep = self.pack[item.name.upper()] - selection = f"[{rep}]" if i == self.menu.position else f" {rep} " + selection = f"[{rep}]" if i == self.menu.position \ + and self.selected else f" {rep} " self.addstr(self.pad, i + 1, 0, selection + " " + item.translated_name.capitalize() + ": " + str(item.price) + " Hazels") + price = f"{self.pack.HAZELNUT} {self.menu.merchant.hazel} Hazels" + width = len(price) + (self.pack.tile_width - 1) + self.addstr(self.pad, self.height - 3, self.width - width - 2, price, + italic=True) + @property def truewidth(self) -> int: return max(1, self.height if hasattr(self, "height") else 10) @@ -193,4 +237,5 @@ class StoreInventoryDisplay(MenuDisplay): We can select a menu item with the mouse. """ self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2)) + game.is_in_store_menu = True game.handle_key_pressed(KeyValues.ENTER) diff --git a/squirrelbattle/display/messagedisplay.py b/squirrelbattle/display/messagedisplay.py index 74a98a9..d850500 100644 --- a/squirrelbattle/display/messagedisplay.py +++ b/squirrelbattle/display/messagedisplay.py @@ -3,6 +3,7 @@ import curses from squirrelbattle.display.display import Box, Display +from squirrelbattle.game import Game class MessageDisplay(Display): @@ -17,8 +18,8 @@ class MessageDisplay(Display): self.message = "" self.pad = self.newpad(1, 1) - def update_message(self, msg: str) -> None: - self.message = msg + def update(self, game: Game) -> None: + self.message = game.message def display(self) -> None: self.box.refresh(self.y - 1, self.x - 2, diff --git a/squirrelbattle/display/statsdisplay.py b/squirrelbattle/display/statsdisplay.py index a2fd5f4..b6ca30a 100644 --- a/squirrelbattle/display/statsdisplay.py +++ b/squirrelbattle/display/statsdisplay.py @@ -4,6 +4,7 @@ import curses from ..entities.player import Player +from ..game import Game from ..translations import gettext as _ from .display import Display @@ -18,8 +19,8 @@ class StatsDisplay(Display): super().__init__(*args, **kwargs) self.pad = self.newpad(self.rows, self.cols) - def update_player(self, p: Player) -> None: - self.player = p + def update(self, game: Game) -> None: + self.player = game.player def update_pad(self) -> None: string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\ diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 449a2b7..c4d68f1 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -102,7 +102,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( MERCHANT='🦜', RABBIT='🐇', SUNFLOWER='🌻', - SWORD='🗡️', + SWORD='🗡️ ', TEDDY_BEAR='🧸', TIGER='🐅', TRUMPET='🎺', diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 03dbfe7..b790763 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -34,6 +34,7 @@ class Game: """ self.state = GameMode.MAINMENU self.waiting_for_friendly_key = False + self.is_in_store_menu = True self.settings = Settings() self.settings.load_settings() self.settings.write_settings() @@ -51,7 +52,7 @@ class Game: Creates a new game on the screen. """ # TODO generate a new map procedurally - self.map = Map.load(ResourceManager.get_asset_path("example_map.txt")) + self.map = Map.load(ResourceManager.get_asset_path("example_map_2.txt")) self.map.logs = self.logs self.logs.clear() self.player = Player() @@ -60,16 +61,18 @@ class Game: self.map.spawn_random_entities(randint(3, 10)) self.inventory_menu.update_player(self.player) - def run(self, screen: Any) -> None: + def run(self, screen: Any) -> None: # pragma no cover """ Main infinite loop. We wait for the player's action, then we do what should be done when a key gets pressed. """ - while True: # pragma no cover + screen.refresh() + while True: screen.erase() - screen.refresh() + screen.noutrefresh() self.display_actions(DisplayActions.REFRESH) + curses.doupdate() key = screen.getkey() if key == "KEY_MOUSE": _ignored1, x, y, _ignored2, _ignored3 = curses.getmouse() @@ -165,7 +168,9 @@ class Game: self.logs.add_message(msg) if entity.is_merchant(): self.state = GameMode.STORE + self.is_in_store_menu = True self.store_menu.update_merchant(entity) + self.display_actions(DisplayActions.UPDATE) def handle_key_pressed_inventory(self, key: KeyValues) -> None: """ @@ -194,22 +199,33 @@ class Game: """ In a store menu, we can buy items or close the menu. """ - if key == KeyValues.SPACE: + menu = self.store_menu if self.is_in_store_menu else self.inventory_menu + + if key == KeyValues.SPACE or key == KeyValues.INVENTORY: self.state = GameMode.PLAY elif key == KeyValues.UP: - self.store_menu.go_up() + menu.go_up() elif key == KeyValues.DOWN: - self.store_menu.go_down() - if self.store_menu.values and not self.player.dead: + menu.go_down() + elif key == KeyValues.LEFT: + self.is_in_store_menu = False + self.display_actions(DisplayActions.UPDATE) + elif key == KeyValues.RIGHT: + self.is_in_store_menu = True + self.display_actions(DisplayActions.UPDATE) + if menu.values and not self.player.dead: if key == KeyValues.ENTER: - item = self.store_menu.validate() - flag = item.be_sold(self.player, self.store_menu.merchant) + item = menu.validate() + owner = self.store_menu.merchant if self.is_in_store_menu \ + else self.player + buyer = self.player if self.is_in_store_menu \ + else self.store_menu.merchant + flag = item.be_sold(buyer, owner) if not flag: - self.message = _("You do not have enough money") - self.display_actions(DisplayActions.UPDATE) + self.message = _("The buyer does not have enough money") + self.display_actions(DisplayActions.UPDATE) # Ensure that the cursor has a good position - self.store_menu.position = min(self.store_menu.position, - len(self.store_menu.values) - 1) + menu.position = min(menu.position, len(menu.values) - 1) def handle_key_pressed_main_menu(self, key: KeyValues) -> None: """ diff --git a/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po index 39cfeec..29bf10e 100644 --- a/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po @@ -59,8 +59,8 @@ msgid "{player} exchanged its body with {entity}." msgstr "{player} täuscht seinem Körper mit {entity} aus." #: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 -msgid "You do not have enough money" -msgstr "Sie haben nicht genug Geld" +msgid "The buyer does not have enough money" +msgstr "Der Kaufer hat nicht genug Geld" #: squirrelbattle/game.py:249 msgid "" diff --git a/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po index f4d1c3c..8a7bc3e 100644 --- a/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po @@ -58,8 +58,8 @@ msgid "{player} exchanged its body with {entity}." msgstr "{player} intercambió su cuerpo con {entity}." #: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 -msgid "You do not have enough money" -msgstr "No tienes suficiente dinero" +msgid "The buyer does not have enough money" +msgstr "El comprador no tiene suficiente dinero" #: squirrelbattle/game.py:249 msgid "" diff --git a/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po index 8b1c4db..ffbfdce 100644 --- a/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po @@ -59,8 +59,8 @@ msgid "{player} exchanged its body with {entity}." msgstr "{player} a échangé son corps avec {entity}." #: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 -msgid "You do not have enough money" -msgstr "Vous n'avez pas assez d'argent" +msgid "The buyer does not have enough money" +msgstr "L'acheteur n'a pas assez d'argent" #: squirrelbattle/game.py:249 msgid "" diff --git a/squirrelbattle/menus.py b/squirrelbattle/menus.py index e6e8cef..7732642 100644 --- a/squirrelbattle/menus.py +++ b/squirrelbattle/menus.py @@ -144,7 +144,7 @@ class StoreMenu(Menu): """ A special instance of a menu : the menu for the inventory of a merchant. """ - merchant: Merchant + merchant: Merchant = None def update_merchant(self, merchant: Merchant) -> None: """ @@ -157,4 +157,4 @@ class StoreMenu(Menu): """ Returns the values of the menu. """ - return self.merchant.inventory + return self.merchant.inventory if self.merchant else [] diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 54e18d6..b51a46a 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -12,6 +12,7 @@ from ..entities.items import Bomb, Heart, Sword, Explosion from ..entities.player import Player from ..enums import DisplayActions from ..game import Game, KeyValues, GameMode +from ..interfaces import Map from ..menus import MainMenuValues from ..resources import ResourceManager from ..settings import Settings @@ -25,6 +26,10 @@ class TestGame(unittest.TestCase): """ self.game = Game() self.game.new_game() + self.game.map = Map.load( + ResourceManager.get_asset_path("example_map.txt")) + self.game.map.add_entity(self.game.player) + self.game.player.move(self.game.map.start_y, self.game.map.start_x) self.game.logs.add_message("Hello World !") display = DisplayManager(None, self.game) self.game.display_actions = display.handle_display_action @@ -404,6 +409,7 @@ class TestGame(unittest.TestCase): Checks that some functions are not implemented, only for coverage. """ self.assertRaises(NotImplementedError, Display.display, None) + self.assertRaises(NotImplementedError, Display.update, None, self.game) def test_messages(self) -> None: """ @@ -551,18 +557,21 @@ class TestGame(unittest.TestCase): # Navigate in the menu self.game.handle_key_pressed(KeyValues.DOWN) self.game.handle_key_pressed(KeyValues.DOWN) + self.game.handle_key_pressed(KeyValues.LEFT) + self.assertFalse(self.game.is_in_store_menu) + self.game.handle_key_pressed(KeyValues.RIGHT) + self.assertTrue(self.game.is_in_store_menu) self.game.handle_key_pressed(KeyValues.UP) self.assertEqual(self.game.store_menu.position, 1) self.game.player.hazel = 0x7ffff42ff # The second item is not a heart - merchant.inventory[1] = Sword() + merchant.inventory[1] = sword = Sword() # 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, 7, 25) - self.game.handle_key_pressed(KeyValues.ENTER) self.assertIn(item, self.game.player.inventory) self.assertNotIn(item, merchant.inventory) @@ -584,9 +593,24 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.ENTER) self.assertNotIn(item, self.game.player.inventory) self.assertIn(item, merchant.inventory) - self.assertEqual(self.game.message, _("You do not have enough money")) + self.assertEqual(self.game.message, + _("The buyer does not have enough money")) self.game.handle_key_pressed(KeyValues.ENTER) + # Sell an item + self.game.inventory_menu.position = len(self.game.player.inventory) - 1 + self.game.handle_key_pressed(KeyValues.LEFT) + self.assertFalse(self.game.is_in_store_menu) + self.assertIn(sword, self.game.player.inventory) + self.assertEqual(self.game.inventory_menu.validate(), sword) + old_player_money, old_merchant_money = self.game.player.hazel,\ + merchant.hazel + self.game.handle_key_pressed(KeyValues.ENTER) + self.assertNotIn(sword, self.game.player.inventory) + self.assertIn(sword, merchant.inventory) + self.assertEqual(self.game.player.hazel, old_player_money + sword.price) + self.assertEqual(merchant.hazel, old_merchant_money - sword.price) + # Exit the menu self.game.handle_key_pressed(KeyValues.SPACE) self.assertEqual(self.game.state, GameMode.PLAY) diff --git a/squirrelbattle/tests/screen.py b/squirrelbattle/tests/screen.py index 386b3d3..57b7dcc 100644 --- a/squirrelbattle/tests/screen.py +++ b/squirrelbattle/tests/screen.py @@ -12,8 +12,8 @@ class FakePad: def addstr(self, y: int, x: int, message: str, color: int = 0) -> None: pass - def refresh(self, pminrow: int, pmincol: int, sminrow: int, - smincol: int, smaxrow: int, smaxcol: int) -> None: + def noutrefresh(self, pminrow: int, pmincol: int, sminrow: int, + smincol: int, smaxrow: int, smaxcol: int) -> None: pass def erase(self) -> None: