diff --git a/squirrelbattle/display/display.py b/squirrelbattle/display/display.py index 9cc1456..29295de 100644 --- a/squirrelbattle/display/display.py +++ b/squirrelbattle/display/display.py @@ -2,9 +2,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later import curses -from typing import Any, Optional, Union +from typing import Any, Optional, Tuple, Union from squirrelbattle.display.texturepack import TexturePack +from squirrelbattle.game import Game from squirrelbattle.tests.screen import FakePad @@ -15,6 +16,9 @@ class Display: height: int pad: Any + _color_pairs = {(curses.COLOR_WHITE, curses.COLOR_BLACK): 0} + _colors_rgb = {} + def __init__(self, screen: Any, pack: Optional[TexturePack] = None): self.screen = screen self.pack = pack or TexturePack.get_pack("ascii") @@ -30,15 +34,84 @@ class Display: lines = [line[:width] for line in lines] return "\n".join(lines) - def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None: + def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int: + """ + Translate a tuple (R, G, B) into a curses color index. + If we have already a color index, then nothing is processed. + If this is a tuple, we construct a new color index if non-existing + and we return this index. + The values of R, G and B must be between 0 and 1000, and not + between 0 and 255. + """ + if isinstance(color, tuple): + # The color is a tuple (R, G, B), that is potentially unknown. + # We translate it into a curses color number. + if color not in self._colors_rgb: + # The color does not exist, we create it. + color_nb = len(self._colors_rgb) + 8 + self.init_color(color_nb, color[0], color[1], color[2]) + self._colors_rgb[color] = color_nb + color = self._colors_rgb[color] + return color + + def addstr(self, pad: Any, y: int, x: int, msg: str, + fg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_WHITE, + bg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_BLACK, + *, altcharset: bool = False, blink: bool = False, + bold: bool = False, dim: bool = False, invis: bool = False, + italic: bool = False, normal: bool = False, + protect: bool = False, reverse: bool = False, + standout: bool = False, underline: bool = False, + horizontal: bool = False, left: bool = False, + low: bool = False, right: bool = False, top: bool = False, + vertical: bool = False, chartext: bool = False) -> None: """ Display a message onto the pad. If the message is too large, it is truncated vertically and horizontally + The text can be bold, italic, blinking, ... if the good parameters are + given. These parameters are translated into curses attributes. + The foreground and background colors can be given as curses constants + (curses.COLOR_*), or by giving a tuple (R, G, B) that corresponds to + the color. R, G, B must be between 0 and 1000, and not 0 and 255. """ height, width = pad.getmaxyx() + # Truncate message if it is too large msg = self.truncate(msg, height - y, width - x - 1) if msg.replace("\n", "") and x >= 0 and y >= 0: - return pad.addstr(y, x, msg, *options) + fg_color = self.translate_color(fg_color) + bg_color = self.translate_color(bg_color) + + # Get the pair number for the tuple (fg, bg) + # If it does not exist, create it and give a new unique id. + if (fg_color, bg_color) in self._color_pairs: + pair_nb = self._color_pairs[(fg_color, bg_color)] + else: + pair_nb = len(self._color_pairs) + self.init_pair(pair_nb, fg_color, bg_color) + self._color_pairs[(fg_color, bg_color)] = pair_nb + + # Compute curses attributes from the parameters + attr = self.color_pair(pair_nb) + attr |= curses.A_ALTCHARSET if altcharset else 0 + attr |= curses.A_BLINK if blink else 0 + attr |= curses.A_BOLD if bold else 0 + attr |= curses.A_DIM if dim else 0 + attr |= curses.A_INVIS if invis else 0 + attr |= curses.A_ITALIC if italic else 0 + attr |= curses.A_NORMAL if normal else 0 + attr |= curses.A_PROTECT if protect else 0 + attr |= curses.A_REVERSE if reverse else 0 + attr |= curses.A_STANDOUT if standout else 0 + attr |= curses.A_UNDERLINE if underline else 0 + attr |= curses.A_HORIZONTAL if horizontal else 0 + attr |= curses.A_LEFT if left else 0 + attr |= curses.A_LOW if low else 0 + attr |= curses.A_RIGHT if right else 0 + attr |= curses.A_TOP if top else 0 + attr |= curses.A_VERTICAL if vertical else 0 + attr |= curses.A_CHARTEXT if chartext else 0 + + return pad.addstr(y, x, msg, attr) def init_pair(self, number: int, foreground: int, background: int) -> None: return curses.init_pair(number, foreground, background) \ @@ -47,6 +120,10 @@ class Display: def color_pair(self, number: int) -> int: return curses.color_pair(number) if self.screen else 0 + def init_color(self, number: int, red: int, green: int, blue: int) -> None: + return curses.init_color(number, red, green, blue) \ + if self.screen else None + def resize(self, y: int, x: int, height: int, width: int, resize_pad: bool = True) -> None: self.x = x @@ -55,6 +132,7 @@ class Display: self.height = height if hasattr(self, "pad") and resize_pad and \ self.height >= 0 and self.width >= 0: + self.pad.erase() self.pad.resize(self.height + 1, self.width + 1) def refresh(self, *args, resize_pad: bool = True) -> None: @@ -86,6 +164,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 @@ -138,23 +223,29 @@ class HorizontalSplit(Display): class Box(Display): + title: str = "" + + def update_title(self, title: str) -> None: + self.title = title def __init__(self, *args, fg_border_color: Optional[int] = None, **kwargs): super().__init__(*args, **kwargs) self.pad = self.newpad(self.rows, self.cols) self.fg_border_color = fg_border_color or curses.COLOR_WHITE - pair_number = 4 + self.fg_border_color - self.init_pair(pair_number, self.fg_border_color, curses.COLOR_BLACK) - self.pair = self.color_pair(pair_number) - def display(self) -> None: self.addstr(self.pad, 0, 0, "┏" + "━" * (self.width - 2) + "┓", - self.pair) + self.fg_border_color) for i in range(1, self.height - 1): - self.addstr(self.pad, i, 0, "┃", self.pair) - self.addstr(self.pad, i, self.width - 1, "┃", self.pair) + self.addstr(self.pad, i, 0, "┃", self.fg_border_color) + self.addstr(self.pad, i, self.width - 1, "┃", self.fg_border_color) self.addstr(self.pad, self.height - 1, 0, - "┗" + "━" * (self.width - 2) + "┛", self.pair) + "┗" + "━" * (self.width - 2) + "┛", self.fg_border_color) + + if self.title: + self.addstr(self.pad, 0, (self.width - len(self.title) - 8) // 2, + f" == {self.title} == ", curses.COLOR_GREEN, + italic=True, bold=True) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.y + self.height - 1, self.x + self.width - 1) diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index 0e9cf04..f9b3f01 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -2,15 +2,16 @@ # 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 from squirrelbattle.display.menudisplay import MainMenuDisplay, \ - InventoryDisplay, SettingsMenuDisplay + 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 @@ -24,7 +25,8 @@ class DisplayManager: self.mapdisplay = MapDisplay(screen, pack) self.statsdisplay = StatsDisplay(screen, pack) self.logsdisplay = LogsDisplay(screen, pack) - self.inventorydisplay = InventoryDisplay(screen, pack) + self.playerinventorydisplay = PlayerInventoryDisplay(screen, pack) + self.storeinventorydisplay = StoreInventoryDisplay(screen, pack) self.mainmenudisplay = MainMenuDisplay(self.game.main_menu, screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) @@ -33,28 +35,50 @@ class DisplayManager: self.vbar = VerticalSplit(screen, pack) self.displays = [self.statsdisplay, self.mapdisplay, self.mainmenudisplay, self.settingsmenudisplay, - self.logsdisplay, self.messagedisplay] + self.logsdisplay, self.messagedisplay, + self.playerinventorydisplay, + 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: 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.inventorydisplay.update_menu(self.game.inventory_menu) + 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) - 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.INVENTORY \ + or self.game.state == GameMode.STORE: # The map pad has already the good size self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.mapdisplay.pack.tile_width @@ -67,15 +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.inventorydisplay.refresh(self.rows // 10, - self.cols // 2, - 8 * self.rows // 10, - 2 * self.cols // 5) + 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 @@ -84,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. diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index d403f7f..54d9432 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -15,15 +15,14 @@ class MapDisplay(Display): self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1) def update_pad(self) -> None: - self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color) - self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color) self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack), - self.color_pair(1)) + self.pack.tile_fg_color, self.pack.tile_bg_color) for e in self.map.entities: self.addstr(self.pad, e.y, self.pack.tile_width * e.x, - self.pack[e.name.upper()], self.color_pair(2)) + self.pack[e.name.upper()], + self.pack.entity_fg_color, self.pack.entity_bg_color) - # Display Path map for deubg purposes + # Display Path map for debug purposes # from squirrelbattle.entities.player import Player # players = [ p for p in self.map.entities if isinstance(p,Player) ] # player = players[0] if len(players) > 0 else None @@ -42,7 +41,8 @@ class MapDisplay(Display): # else: # character = '←' # self.addstr(self.pad, y, self.pack.tile_width * x, - # character, self.color_pair(1)) + # character, self.pack.tile_fg_color, + # self.pack.tile_bg_color) def display(self) -> None: y, x = self.map.currenty, self.pack.tile_width * self.map.currentx diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index d040d81..a00d0fe 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -1,15 +1,22 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later + import curses +from random import randint from typing import List from squirrelbattle.menus import Menu, MainMenu -from .display import Display, Box +from .display import Box, Display +from ..enums import KeyValues +from ..game import Game from ..resources import ResourceManager from ..translations import gettext as _ class MenuDisplay(Display): + """ + A class to display the menu objects + """ position: int def __init__(self, *args, **kwargs): @@ -24,9 +31,9 @@ class MenuDisplay(Display): def update_pad(self) -> None: for i in range(self.trueheight): - self.addstr(self.pad, i, 0, " " + self.values[i]) + self.addstr(self.pad, i, 0, " " + self.values[i]) # set a marker on the selected line - self.addstr(self.pad, self.menu.position, 0, ">") + self.addstr(self.pad, self.menu.position, 0, " >") def display(self) -> None: cornery = 0 if self.height - 2 >= self.menu.position - 1 \ @@ -37,10 +44,17 @@ class MenuDisplay(Display): self.menubox.refresh(self.y, self.x, self.height, self.width) self.pad.erase() self.update_pad() - self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2, + self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 1, 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]) @@ -63,6 +77,9 @@ class MenuDisplay(Display): class SettingsMenuDisplay(MenuDisplay): + """ + A class to display specifically a settingsmenu object + """ @property def values(self) -> List[str]: return [_(a[1][1]) + (" : " @@ -73,6 +90,9 @@ class SettingsMenuDisplay(MenuDisplay): class MainMenuDisplay(Display): + """ + A class to display specifically a mainmenu object + """ def __init__(self, menu: MainMenu, *args): super().__init__(*args) self.menu = menu @@ -83,13 +103,16 @@ class MainMenuDisplay(Display): self.pad = self.newpad(max(self.rows, len(self.title) + 30), max(len(self.title[0]) + 5, self.cols)) + self.fg_color = curses.COLOR_WHITE + self.menudisplay = MenuDisplay(self.screen, self.pack) self.menudisplay.update_menu(self.menu) def display(self) -> None: for i in range(len(self.title)): self.addstr(self.pad, 4 + i, max(self.width // 2 - - len(self.title[0]) // 2 - 1, 0), self.title[i]) + - len(self.title[0]) // 2 - 1, 0), self.title[i], + self.fg_color) self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.height + self.y - 1, self.width + self.x - 1) @@ -99,16 +122,25 @@ 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) -class InventoryDisplay(MenuDisplay): + if menuy <= y < menuy + menuheight and menux <= x < menux + menuwidth: + self.menudisplay.handle_click(y - menuy, x - menux, game) + + if y <= len(self.title): + self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000) + + +class PlayerInventoryDisplay(MenuDisplay): def update_pad(self) -> None: - message = _("== INVENTORY ==") - self.addstr(self.pad, 0, (self.width - len(message)) // 2, message, - curses.A_BOLD | curses.A_ITALIC) + 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} " - self.addstr(self.pad, 2 + i, 0, selection + self.addstr(self.pad, i + 1, 0, selection + " " + item.translated_name.capitalize()) @property @@ -118,3 +150,36 @@ 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 - 2)) + game.handle_key_pressed(KeyValues.ENTER) + + +class StoreInventoryDisplay(MenuDisplay): + 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} " + self.addstr(self.pad, i + 1, 0, selection + + " " + item.translated_name.capitalize() + + ": " + str(item.price) + " Hazels") + + @property + def truewidth(self) -> int: + return max(1, self.height if hasattr(self, "height") else 10) + + @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 - 2)) + game.handle_key_pressed(KeyValues.ENTER) diff --git a/squirrelbattle/display/messagedisplay.py b/squirrelbattle/display/messagedisplay.py index bcc2539..32f7139 100644 --- a/squirrelbattle/display/messagedisplay.py +++ b/squirrelbattle/display/messagedisplay.py @@ -25,7 +25,7 @@ class MessageDisplay(Display): self.height + 2, self.width + 4) self.box.display() self.pad.erase() - self.addstr(self.pad, 0, 0, self.message, curses.A_BOLD) + self.addstr(self.pad, 0, 0, self.message, bold=True) self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.height + self.y - 1, self.width + self.x - 1) diff --git a/squirrelbattle/display/statsdisplay.py b/squirrelbattle/display/statsdisplay.py index ac1a89c..9937c3e 100644 --- a/squirrelbattle/display/statsdisplay.py +++ b/squirrelbattle/display/statsdisplay.py @@ -14,7 +14,6 @@ class StatsDisplay(Display): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.pad = self.newpad(self.rows, self.cols) - self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) def update_player(self, p: Player) -> None: self.player = p @@ -46,10 +45,12 @@ class StatsDisplay(Display): printed_items.append(item) self.addstr(self.pad, 8, 0, inventory_str) + self.addstr(self.pad, 9, 0, f"{self.pack.HAZELNUT} " + f"x{self.player.hazel}") + if self.player.dead: - self.addstr(self.pad, 10, 0, _("YOU ARE DEAD"), - curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT - | self.color_pair(3)) + self.addstr(self.pad, 11, 0, _("YOU ARE DEAD"), curses.COLOR_RED, + bold=True, blink=True, standout=True) def display(self) -> None: self.pad.erase() diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 7fc4a9a..f72cd97 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -14,10 +14,22 @@ class TexturePack: tile_bg_color: int entity_fg_color: int entity_bg_color: int + + BODY_SNATCH_POTION: str + BOMB: str + HEART: str + HEDGEHOG: str EMPTY: str - WALL: str FLOOR: str + HAZELNUT: str + MERCHANT: str PLAYER: str + RABBIT: str + SUNFLOWER: str + SWORD: str + TEDDY_BEAR: str + TIGER: str + WALL: str ASCII_PACK: "TexturePack" SQUIRREL_PACK: "TexturePack" @@ -46,17 +58,23 @@ TexturePack.ASCII_PACK = TexturePack( tile_bg_color=curses.COLOR_BLACK, entity_fg_color=curses.COLOR_WHITE, entity_bg_color=curses.COLOR_BLACK, - EMPTY=' ', - WALL='#', - FLOOR='.', - PLAYER='@', - HEDGEHOG='*', - HEART='❤', - BOMB='o', - RABBIT='Y', - TIGER='n', - TEDDY_BEAR='8', + BODY_SNATCH_POTION='S', + BOMB='o', + EMPTY=' ', + EXPLOSION='%', + FLOOR='.', + HAZELNUT='¤', + HEART='❤', + HEDGEHOG='*', + MERCHANT='M', + PLAYER='@', + RABBIT='Y', + SUNFLOWER='I', + SWORD='\u2020', + TEDDY_BEAR='8', + TIGER='n', + WALL='#', ) TexturePack.SQUIRREL_PACK = TexturePack( @@ -66,15 +84,21 @@ TexturePack.SQUIRREL_PACK = TexturePack( tile_bg_color=curses.COLOR_BLACK, entity_fg_color=curses.COLOR_WHITE, entity_bg_color=curses.COLOR_WHITE, - EMPTY=' ', - WALL='🧱', - FLOOR='██', - PLAYER='🐿️ ️', - HEDGEHOG='🦔', - HEART='💜', - BOMB='💣', - RABBIT='🐇', - TIGER='🐅', - TEDDY_BEAR='🧸', + BODY_SNATCH_POTION='🔀', + BOMB='💣', + EMPTY=' ', + EXPLOSION='💥', + FLOOR='██', + HAZELNUT='🌰', + HEART='💜', + HEDGEHOG='🦔', + PLAYER='🐿️ ️', + MERCHANT='🦜', + RABBIT='🐇', + SUNFLOWER='🌻', + SWORD='🗡️', + TEDDY_BEAR='🧸', + TIGER='🐅', + WALL='🧱', ) diff --git a/squirrelbattle/entities/friendly.py b/squirrelbattle/entities/friendly.py new file mode 100644 index 0000000..6c99090 --- /dev/null +++ b/squirrelbattle/entities/friendly.py @@ -0,0 +1,52 @@ +from ..interfaces import FriendlyEntity, InventoryHolder +from ..translations import gettext as _ +from .player import Player +from .items import Item +from random import choice + + +class Merchant(InventoryHolder, FriendlyEntity): + """ + The class for merchants in the dungeon + """ + def keys(self) -> list: + """ + Returns a friendly entitie's specific attributes + """ + return super().keys() + ["inventory", "hazel"] + + def __init__(self, name: str = "merchant", inventory: list = None, + hazel: int = 75, *args, **kwargs): + super().__init__(name=name, *args, **kwargs) + self.inventory = self.translate_inventory(inventory or []) + self.hazel = hazel + + if not self.inventory: + for i in range(5): + self.inventory.append(choice(Item.get_all_items())()) + + def talk_to(self, player: Player) -> str: + """ + This function is used to open the merchant's inventory in a menu, + and allow the player to buy/sell objects + """ + return _("I don't sell any squirrel") + + def change_hazel_balance(self, hz: int) -> None: + """ + Change the number of hazel the merchant has by hz. + """ + self.hazel += hz + + +class Sunflower(FriendlyEntity): + """ + A friendly sunflower + """ + def __init__(self, maxhealth: int = 15, + *args, **kwargs) -> None: + super().__init__(name="sunflower", maxhealth=maxhealth, *args, **kwargs) + + @property + def dialogue_option(self) -> list: + return [_("Flower power!!"), _("The sun is warm today")] diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index e90ec32..865a703 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -5,7 +5,7 @@ from random import choice, randint from typing import Optional from .player import Player -from ..interfaces import Entity, FightingEntity, Map +from ..interfaces import Entity, FightingEntity, Map, InventoryHolder from ..translations import gettext as _ @@ -14,13 +14,16 @@ class Item(Entity): A class for items """ held: bool - held_by: Optional[Player] + held_by: Optional[InventoryHolder] + price: int - def __init__(self, held: bool = False, held_by: Optional[Player] = None, - *args, **kwargs): + def __init__(self, held: bool = False, + held_by: Optional[InventoryHolder] = None, + price: int = 2, *args, **kwargs): super().__init__(*args, **kwargs) self.held = held self.held_by = held_by + self.price = price def drop(self) -> None: """ @@ -28,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 @@ -43,14 +46,14 @@ class Item(Entity): Indicates what should be done when the item is equipped. """ - def hold(self, player: "Player") -> None: + def hold(self, player: InventoryHolder) -> None: """ The item is taken from the floor and put into the inventory """ self.held = True self.held_by = player - self.map.remove_entity(self) - player.inventory.append(self) + self.held_by.map.remove_entity(self) + player.add_to_inventory(self) def save_state(self) -> dict: """ @@ -60,6 +63,25 @@ class Item(Entity): d["held"] = self.held return d + @staticmethod + def get_all_items() -> list: + return [BodySnatchPotion, Bomb, Heart, Sword] + + def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: + """ + Does all necessary actions when an object is to be sold. + Is overwritten by some classes that cannot exist in the player's + inventory + """ + if buyer.hazel >= self.price: + self.hold(buyer) + seller.remove_from_inventory(self) + buyer.change_hazel_balance(-self.price) + seller.change_hazel_balance(self.price) + return True + else: + return False + class Heart(Item): """ @@ -67,16 +89,17 @@ class Heart(Item): """ healing: int - def __init__(self, name: str = "heart", healing: int = 5, *args, **kwargs): - super().__init__(name=name, *args, **kwargs) + def __init__(self, name: str = "heart", healing: int = 5, price: int = 3, + *args, **kwargs): + super().__init__(name=name, price=price, *args, **kwargs) self.healing = healing - def hold(self, player: "Player") -> None: + def hold(self, entity: InventoryHolder) -> None: """ When holding a heart, heal the player and don't put item in inventory. """ - player.health = min(player.maxhealth, player.health + self.healing) - self.map.remove_entity(self) + entity.health = min(entity.maxhealth, entity.health + self.healing) + entity.map.remove_entity(self) def save_state(self) -> dict: """ @@ -97,8 +120,8 @@ class Bomb(Item): tick: int def __init__(self, name: str = "bomb", damage: int = 5, - exploding: bool = False, *args, **kwargs): - super().__init__(name=name, *args, **kwargs) + exploding: bool = False, price: int = 4, *args, **kwargs): + super().__init__(name=name, price=price, *args, **kwargs) self.damage = damage self.exploding = exploding self.tick = 4 @@ -135,6 +158,10 @@ class Bomb(Item): m.logs.add_message(log_message) m.entities.remove(self) + # Add sparkles where the bomb exploded. + explosion = Explosion(y=self.y, x=self.x) + self.map.add_entity(explosion) + def save_state(self) -> dict: """ Saves the state of the bomb into a dictionary @@ -145,14 +172,63 @@ class Bomb(Item): return d +class Explosion(Item): + """ + When a bomb explodes, the explosion is displayed. + """ + def __init__(self, *args, **kwargs): + super().__init__(name="explosion", *args, **kwargs) + + def act(self, m: Map) -> None: + """ + The explosion instant dies. + """ + m.remove_entity(self) + + def hold(self, player: InventoryHolder) -> None: + """ + The player can't hold any explosion. + """ + pass + + +class Weapon(Item): + """ + Non-throwable items that improve player damage + """ + damage: int + + def __init__(self, damage: int = 3, *args, **kwargs): + super().__init__(*args, **kwargs) + self.damage = damage + + def save_state(self) -> dict: + """ + Saves the state of the weapon into a dictionary + """ + d = super().save_state() + d["damage"] = self.damage + return d + + +class Sword(Weapon): + """ + A basic weapon + """ + def __init__(self, name: str = "sword", price: int = 20, *args, **kwargs): + super().__init__(name=name, price=price, *args, **kwargs) + self.name = name + + class BodySnatchPotion(Item): """ The body-snatch potion allows to exchange all characteristics with a random other entity. """ - def __init__(self, name: str = "body_snatch_potion", *args, **kwargs): - super().__init__(name=name, *args, **kwargs) + def __init__(self, name: str = "body_snatch_potion", price: int = 14, + *args, **kwargs): + super().__init__(name=name, price=price, *args, **kwargs) def use(self) -> None: """ diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 45e2bdf..19c8348 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -6,23 +6,22 @@ from queue import PriorityQueue from random import randint from typing import Dict, Tuple -from ..interfaces import FightingEntity +from ..interfaces import FightingEntity, InventoryHolder -class Player(FightingEntity): +class Player(InventoryHolder, FightingEntity): """ The class of the player """ current_xp: int = 0 max_xp: int = 10 - inventory: list paths: Dict[Tuple[int, int], Tuple[int, int]] def __init__(self, name: str = "player", maxhealth: int = 20, strength: int = 5, intelligence: int = 1, charisma: int = 1, dexterity: int = 1, constitution: int = 1, level: int = 1, current_xp: int = 0, max_xp: int = 10, inventory: list = None, - *args, **kwargs) \ + hazel: int = 42, *args, **kwargs) \ -> None: super().__init__(name=name, maxhealth=maxhealth, strength=strength, intelligence=intelligence, charisma=charisma, @@ -30,13 +29,9 @@ class Player(FightingEntity): level=level, *args, **kwargs) self.current_xp = current_xp self.max_xp = max_xp - self.inventory = inventory if inventory else list() - for i in range(len(self.inventory)): - if isinstance(self.inventory[i], dict): - entity_classes = self.get_all_entity_classes_in_a_dict() - item_class = entity_classes[self.inventory[i]["type"]] - self.inventory[i] = item_class(**self.inventory[i]) + self.inventory = self.translate_inventory(inventory or []) self.paths = dict() + self.hazel = hazel def move(self, y: int, x: int) -> None: """ @@ -149,5 +144,4 @@ class Player(FightingEntity): d = super().save_state() d["current_xp"] = self.current_xp d["max_xp"] = self.max_xp - d["inventory"] = [item.save_state() for item in self.inventory] return d diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index 84eb498..7e4efa4 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): @@ -26,12 +27,14 @@ class GameMode(Enum): PLAY = auto() SETTINGS = auto() INVENTORY = auto() + STORE = auto() class KeyValues(Enum): """ Key values options used in the game """ + MOUSE = auto() UP = auto() DOWN = auto() LEFT = auto() @@ -42,6 +45,8 @@ class KeyValues(Enum): EQUIP = auto() DROP = auto() SPACE = auto() + CHAT = auto() + WAIT = auto() @staticmethod def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]: @@ -72,4 +77,8 @@ class KeyValues(Enum): return KeyValues.DROP elif key == ' ': return KeyValues.SPACE + elif key == settings.KEY_CHAT: + return KeyValues.CHAT + elif key == settings.KEY_WAIT: + return KeyValues.WAIT return None diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 6d9e9e7..ed3b60f 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: @@ -24,14 +24,16 @@ class Game: """ map: Map 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: """ Init the game. """ self.state = GameMode.MAINMENU + self.waiting_for_friendly_key = False self.settings = Settings() self.settings.load_settings() self.settings.write_settings() @@ -40,6 +42,7 @@ class Game: self.settings_menu = menus.SettingsMenu() self.settings_menu.update_values(self.settings) self.inventory_menu = menus.InventoryMenu() + self.store_menu = menus.StoreMenu() self.logs = Logs() self.message = None @@ -68,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: @@ -83,13 +90,19 @@ class Game: return if self.state == GameMode.PLAY: - self.handle_key_pressed_play(key) + if self.waiting_for_friendly_key: + # The player requested to talk with a friendly entity + self.handle_friendly_entity_chat(key) + else: + self.handle_key_pressed_play(key) elif self.state == GameMode.INVENTORY: self.handle_key_pressed_inventory(key) elif self.state == GameMode.MAINMENU: self.handle_key_pressed_main_menu(key) elif self.state == GameMode.SETTINGS: self.settings_menu.handle_key_pressed(key, raw_key, self) + elif self.state == GameMode.STORE: + self.handle_key_pressed_store(key) self.display_actions(DisplayActions.REFRESH) def handle_key_pressed_play(self, key: KeyValues) -> None: @@ -112,6 +125,44 @@ class Game: self.state = GameMode.INVENTORY elif key == KeyValues.SPACE: self.state = GameMode.MAINMENU + elif key == KeyValues.CHAT: + # Wait for the direction of the friendly entity + self.waiting_for_friendly_key = True + elif key == KeyValues.WAIT: + self.map.tick() + + def handle_friendly_entity_chat(self, key: KeyValues) -> None: + """ + If the player is talking to a friendly entity, we get the direction + where the entity is, then we interact with it. + """ + if not self.waiting_for_friendly_key: + return + self.waiting_for_friendly_key = False + + if key == KeyValues.UP: + xp = self.player.x + yp = self.player.y - 1 + elif key == KeyValues.DOWN: + xp = self.player.x + yp = self.player.y + 1 + elif key == KeyValues.LEFT: + xp = self.player.x - 1 + yp = self.player.y + elif key == KeyValues.RIGHT: + xp = self.player.x + 1 + yp = self.player.y + else: + return + if self.map.entity_is_present(yp, xp): + for entity in self.map.entities: + if entity.is_friendly() and entity.x == xp and \ + entity.y == yp: + msg = entity.talk_to(self.player) + self.logs.add_message(msg) + if entity.is_merchant(): + self.state = GameMode.STORE + self.store_menu.update_merchant(entity) def handle_key_pressed_inventory(self, key: KeyValues) -> None: """ @@ -136,6 +187,27 @@ class Game: len(self.inventory_menu.values) - 1) + def handle_key_pressed_store(self, key: KeyValues) -> None: + """ + In a store menu, we can buy items or close the menu. + """ + if key == KeyValues.SPACE: + self.state = GameMode.PLAY + elif key == KeyValues.UP: + self.store_menu.go_up() + elif key == KeyValues.DOWN: + self.store_menu.go_down() + if self.store_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) + if not flag: + self.message = _("You do 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) + def handle_key_pressed_main_menu(self, key: KeyValues) -> None: """ In the main menu, we can navigate through options. diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 91a2188..6458df7 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -4,7 +4,7 @@ from enum import Enum, auto from math import sqrt from random import choice, randint -from typing import List, Optional, Union, Tuple +from typing import List, Optional, Union, Tuple, Any from .display.texturepack import TexturePack from .translations import gettext as _ @@ -93,7 +93,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 @@ -101,12 +102,21 @@ class Map: def is_free(self, y: int, x: int) -> bool: """ - Indicates that the case at the coordinates (y, x) is empty. + Indicates that the tile at the coordinates (y, x) is empty. """ return 0 <= y < self.height and 0 <= x < self.width and \ self.tiles[y][x].can_walk() and \ not any(entity.x == x and entity.y == y for entity in self.entities) + def entity_is_present(self, y: int, x: int) -> bool: + """ + Indicates that the tile at the coordinates (y, x) contains a killable + entity + """ + return 0 <= y < self.height and 0 <= x < self.width and \ + any(entity.x == x and entity.y == y and entity.is_friendly() + for entity in self.entities) + @staticmethod def load(filename: str) -> "Map": """ @@ -152,7 +162,7 @@ class Map: def spawn_random_entities(self, count: int) -> None: """ - Put randomly {count} hedgehogs on the map, where it is available. + Put randomly {count} entities on the map, where it is available. """ for _ignored in range(count): y, x = 0, 0 @@ -459,20 +469,34 @@ class Entity: from squirrelbattle.entities.items import Item return isinstance(self, Item) + def is_friendly(self) -> bool: + """ + Is this entity a friendly entity? + """ + return isinstance(self, FriendlyEntity) + + def is_merchant(self) -> bool: + """ + Is this entity a merchant? + """ + from squirrelbattle.entities.friendly import Merchant + return isinstance(self, Merchant) + @property def translated_name(self) -> str: return _(self.name.replace("_", " ")) @staticmethod - def get_all_entity_classes(): + def get_all_entity_classes() -> list: """ Returns all entities subclasses """ from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart from squirrelbattle.entities.monsters import Tiger, Hedgehog, \ Rabbit, TeddyBear - return [BodySnatchPotion, Bomb, Heart, Hedgehog, - Rabbit, TeddyBear, Tiger] + from squirrelbattle.entities.friendly import Merchant, Sunflower + return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear, + Sunflower, Tiger, Merchant] @staticmethod def get_all_entity_classes_in_a_dict() -> dict: @@ -482,7 +506,9 @@ class Entity: from squirrelbattle.entities.player import Player from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \ TeddyBear - from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart + from squirrelbattle.entities.friendly import Merchant, Sunflower + from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ + Heart, Sword return { "Tiger": Tiger, "Bomb": Bomb, @@ -492,6 +518,9 @@ class Entity: "Rabbit": Rabbit, "TeddyBear": TeddyBear, "Player": Player, + "Merchant": Merchant, + "Sunflower": Sunflower, + "Sword": Sword, } def save_state(self) -> dict: @@ -567,7 +596,7 @@ class FightingEntity(Entity): def keys(self) -> list: """ - Returns a fighting entities specific attributes + Returns a fighting entity's specific attributes """ return ["name", "maxhealth", "health", "level", "strength", "intelligence", "charisma", "dexterity", "constitution"] @@ -580,3 +609,74 @@ class FightingEntity(Entity): for name in self.keys(): d[name] = getattr(self, name) return d + + +class FriendlyEntity(FightingEntity): + """ + Friendly entities are living entities which do not attack the player + """ + dialogue_option: list + + def talk_to(self, player: Any) -> str: + return _("{entity} said: {message}").format( + entity=self.translated_name.capitalize(), + message=choice(self.dialogue_option)) + + def keys(self) -> list: + """ + Returns a friendly entity's specific attributes + """ + return ["maxhealth", "health"] + + +class InventoryHolder(Entity): + hazel: int # Currency of the game + inventory: list + + def translate_inventory(self, inventory: list) -> list: + """ + Translate the JSON-state of the inventory into a list of the items in + the inventory. + """ + for i in range(len(inventory)): + if isinstance(inventory[i], dict): + inventory[i] = self.dict_to_inventory(inventory[i]) + return inventory + + def dict_to_inventory(self, item_dict: dict) -> Entity: + """ + Translate a dict object that contains the state of an item + into an item object. + """ + entity_classes = self.get_all_entity_classes_in_a_dict() + + item_class = entity_classes[item_dict["type"]] + return item_class(**item_dict) + + def save_state(self) -> dict: + """ + We save the inventory of the merchant formatted as JSON + """ + d = super().save_state() + d["hazel"] = self.hazel + d["inventory"] = [item.save_state() for item in self.inventory] + return d + + def add_to_inventory(self, obj: Any) -> None: + """ + Adds an object to inventory + """ + self.inventory.append(obj) + + def remove_from_inventory(self, obj: Any) -> None: + """ + Removes an object from the inventory + """ + self.inventory.remove(obj) + + def change_hazel_balance(self, hz: int) -> None: + """ + Change the number of hazel the entity has by hz. hz is negative + when the player loses money and positive when he gains money + """ + self.hazel += hz diff --git a/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po index 38d16a6..39cfeec 100644 --- a/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po @@ -1,12 +1,14 @@ -# German translation of Squirrel Battle -# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao # This file is distributed under the same license as the squirrelbattle package. +# FIRST AUTHOR , YEAR. # +#, fuzzy msgid "" msgstr "" "Project-Id-Version: squirrelbattle 3.14.1\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" -"POT-Creation-Date: 2020-12-05 14:46+0100\n" +"POT-Creation-Date: 2020-12-12 18:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -15,31 +17,52 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: squirrelbattle/display/menudisplay.py:105 -msgid "== INVENTORY ==" -msgstr "== BESTAND ==" +#: squirrelbattle/display/menudisplay.py:139 +msgid "INVENTORY" +msgstr "BESTAND" -#: squirrelbattle/display/statsdisplay.py:34 +#: squirrelbattle/display/menudisplay.py:164 +msgid "STALL" +msgstr "STAND" + +#: squirrelbattle/display/statsdisplay.py:33 msgid "Inventory:" msgstr "Bestand:" -#: squirrelbattle/display/statsdisplay.py:50 +#: squirrelbattle/display/statsdisplay.py:52 msgid "YOU ARE DEAD" msgstr "SIE WURDEN GESTORBEN" +#. TODO +#: squirrelbattle/entities/friendly.py:33 +msgid "I don't sell any squirrel" +msgstr "Ich verkaufe keinen Eichhörnchen." + +#: squirrelbattle/entities/friendly.py:52 +msgid "Flower power!!" +msgstr "Blumenmacht!!" + +#: squirrelbattle/entities/friendly.py:52 +msgid "The sun is warm today" +msgstr "Die Sonne ist warm heute" + #. The bomb is exploding. #. Each entity that is close to the bomb takes damages. #. The player earn XP if the entity was killed. -#: squirrelbattle/entities/items.py:128 +#: squirrelbattle/entities/items.py:151 msgid "Bomb is exploding." msgstr "Die Bombe explodiert." -#: squirrelbattle/entities/items.py:172 +#: squirrelbattle/entities/items.py:248 #, python-brace-format msgid "{player} exchanged its body with {entity}." msgstr "{player} täuscht seinem Körper mit {entity} aus." -#: squirrelbattle/game.py:177 +#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 +msgid "You do not have enough money" +msgstr "Sie haben nicht genug Geld" + +#: squirrelbattle/game.py:249 msgid "" "Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted." @@ -47,7 +70,7 @@ msgstr "" "In Ihrer Speicherdatei fehlen einige Schlüssel.\n" "Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht." -#: squirrelbattle/game.py:185 +#: squirrelbattle/game.py:257 msgid "" "No player was found on this map!\n" "Maybe you died?" @@ -55,7 +78,7 @@ msgstr "" "Auf dieser Karte wurde kein Spieler gefunden!\n" "Vielleicht sind Sie gestorben?" -#: squirrelbattle/game.py:205 +#: squirrelbattle/game.py:277 msgid "" "The JSON file is not correct.\n" "Your save seems corrupted. It got deleted." @@ -63,27 +86,32 @@ msgstr "" "Die JSON-Datei ist nicht korrekt.\n" "Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht." -#: squirrelbattle/interfaces.py:400 +#: squirrelbattle/interfaces.py:429 #, python-brace-format msgid "{name} hits {opponent}." msgstr "{name} schlägt {opponent}." -#: squirrelbattle/interfaces.py:412 +#: squirrelbattle/interfaces.py:441 #, python-brace-format msgid "{name} takes {amount} damage." msgstr "{name} nimmt {amount} Schadenspunkte." -#: squirrelbattle/interfaces.py:414 +#: squirrelbattle/interfaces.py:443 #, python-brace-format msgid "{name} dies." msgstr "{name} stirbt." -#: squirrelbattle/menus.py:72 +#: squirrelbattle/interfaces.py:477 +#, python-brace-format +msgid "{entity} said: {message}" +msgstr "{entity} hat gesagt: {message}" + +#: squirrelbattle/menus.py:73 msgid "Back" msgstr "Zurück" -#: squirrelbattle/tests/game_test.py:300 squirrelbattle/tests/game_test.py:303 -#: squirrelbattle/tests/game_test.py:306 +#: squirrelbattle/tests/game_test.py:344 squirrelbattle/tests/game_test.py:347 +#: squirrelbattle/tests/game_test.py:350 squirrelbattle/tests/game_test.py:353 #: squirrelbattle/tests/translations_test.py:16 msgid "New game" msgstr "Neu Spiel" @@ -161,41 +189,65 @@ msgid "Key used to drop an item in the inventory" msgstr "Taste um eines Objekts im Bestand zu werfen" #: squirrelbattle/tests/translations_test.py:53 +msgid "Key used to talk to a friendly entity" +msgstr "Taste um mit einer friedlicher Entität zu sprechen" + +#: squirrelbattle/tests/translations_test.py:55 +msgid "Key used to wait" +msgstr "Wartentaste" + +#: squirrelbattle/tests/translations_test.py:56 msgid "Texture pack" msgstr "Textur-Packung" -#: squirrelbattle/tests/translations_test.py:54 +#: squirrelbattle/tests/translations_test.py:57 msgid "Language" msgstr "Sprache" -#: squirrelbattle/tests/translations_test.py:57 +#: squirrelbattle/tests/translations_test.py:60 msgid "player" msgstr "Spieler" -#: squirrelbattle/tests/translations_test.py:59 -msgid "tiger" -msgstr "Tiger" - -#: squirrelbattle/tests/translations_test.py:60 +#: squirrelbattle/tests/translations_test.py:62 msgid "hedgehog" msgstr "Igel" -#: squirrelbattle/tests/translations_test.py:61 +#: squirrelbattle/tests/translations_test.py:63 +msgid "merchant" +msgstr "Kaufmann" + +#: squirrelbattle/tests/translations_test.py:64 msgid "rabbit" msgstr "Kanninchen" -#: squirrelbattle/tests/translations_test.py:62 +#: squirrelbattle/tests/translations_test.py:65 +msgid "sunflower" +msgstr "Sonnenblume" + +#: squirrelbattle/tests/translations_test.py:66 msgid "teddy bear" msgstr "Teddybär" -#: squirrelbattle/tests/translations_test.py:64 +#: squirrelbattle/tests/translations_test.py:67 +msgid "tiger" +msgstr "Tiger" + +#: squirrelbattle/tests/translations_test.py:69 msgid "body snatch potion" msgstr "Leichenfleddererzaubertrank" -#: squirrelbattle/tests/translations_test.py:65 +#: squirrelbattle/tests/translations_test.py:70 msgid "bomb" msgstr "Bombe" -#: squirrelbattle/tests/translations_test.py:66 +#: squirrelbattle/tests/translations_test.py:71 +msgid "explosion" +msgstr "Explosion" + +#: squirrelbattle/tests/translations_test.py:72 msgid "heart" msgstr "Herz" + +#: squirrelbattle/tests/translations_test.py:73 +msgid "sword" +msgstr "schwert" diff --git a/squirrelbattle/locale/en/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/en/LC_MESSAGES/squirrelbattle.po new file mode 100644 index 0000000..c45e893 --- /dev/null +++ b/squirrelbattle/locale/en/LC_MESSAGES/squirrelbattle.po @@ -0,0 +1,207 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse +# This file is distributed under the same license as the squirrelbattle package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: squirrelbattle 3.14.1\n" +"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" +"POT-Creation-Date: 2020-12-01 17:10+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: squirrelbattle/display/statsdisplay.py:34 +msgid "Inventory:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:39 +msgid "YOU ARE DEAD" +msgstr "" + +#: squirrelbattle/interfaces.py:394 squirrelbattle/interfaces.py:398 +#: squirrelbattle/interfaces.py:408 +#, python-brace-format +msgid "{name} hits {opponent}." +msgstr "" + +#: squirrelbattle/interfaces.py:405 squirrelbattle/interfaces.py:410 +#: squirrelbattle/interfaces.py:420 +#, python-brace-format +msgid "{name} takes {amount} damage." +msgstr "" + +#: squirrelbattle/menus.py:45 squirrelbattle/tests/translations_test.py:14 +#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287 +#: squirrelbattle/tests/translations_test.py:16 +#: squirrelbattle/tests/game_test.py:290 +msgid "New game" +msgstr "" + +#: squirrelbattle/menus.py:46 squirrelbattle/tests/translations_test.py:15 +#: squirrelbattle/tests/translations_test.py:17 +msgid "Resume" +msgstr "" + +#: squirrelbattle/menus.py:47 squirrelbattle/tests/translations_test.py:17 +#: squirrelbattle/tests/translations_test.py:19 +msgid "Save" +msgstr "" + +#: squirrelbattle/menus.py:48 squirrelbattle/tests/translations_test.py:16 +#: squirrelbattle/tests/translations_test.py:18 +msgid "Load" +msgstr "" + +#: squirrelbattle/menus.py:49 squirrelbattle/tests/translations_test.py:18 +#: squirrelbattle/tests/translations_test.py:20 +msgid "Settings" +msgstr "" + +#: squirrelbattle/menus.py:50 squirrelbattle/tests/translations_test.py:19 +#: squirrelbattle/tests/translations_test.py:21 +msgid "Exit" +msgstr "" + +#: squirrelbattle/menus.py:71 +msgid "Back" +msgstr "" + +#: squirrelbattle/game.py:147 squirrelbattle/game.py:148 +msgid "" +"Some keys are missing in your save file.\n" +"Your save seems to be corrupt. It got deleted." +msgstr "" + +#: squirrelbattle/game.py:155 squirrelbattle/game.py:156 +msgid "" +"No player was found on this map!\n" +"Maybe you died?" +msgstr "" + +#: squirrelbattle/game.py:175 squirrelbattle/game.py:176 +msgid "" +"The JSON file is not correct.\n" +"Your save seems corrupted. It got deleted." +msgstr "" + +#: squirrelbattle/settings.py:21 squirrelbattle/tests/translations_test.py:21 +#: squirrelbattle/tests/translations_test.py:25 +#: squirrelbattle/tests/translations_test.py:27 +msgid "Main key to move up" +msgstr "" + +#: squirrelbattle/settings.py:22 squirrelbattle/tests/translations_test.py:23 +#: squirrelbattle/tests/translations_test.py:27 +#: squirrelbattle/tests/translations_test.py:29 +msgid "Secondary key to move up" +msgstr "" + +#: squirrelbattle/settings.py:23 squirrelbattle/tests/translations_test.py:25 +#: squirrelbattle/tests/translations_test.py:29 +#: squirrelbattle/tests/translations_test.py:31 +msgid "Main key to move down" +msgstr "" + +#: squirrelbattle/settings.py:24 squirrelbattle/tests/translations_test.py:27 +#: squirrelbattle/tests/translations_test.py:31 +#: squirrelbattle/tests/translations_test.py:33 +msgid "Secondary key to move down" +msgstr "" + +#: squirrelbattle/settings.py:25 squirrelbattle/tests/translations_test.py:29 +#: squirrelbattle/tests/translations_test.py:33 +#: squirrelbattle/tests/translations_test.py:35 +msgid "Main key to move left" +msgstr "" + +#: squirrelbattle/settings.py:26 squirrelbattle/tests/translations_test.py:31 +#: squirrelbattle/tests/translations_test.py:35 +#: squirrelbattle/tests/translations_test.py:37 +msgid "Secondary key to move left" +msgstr "" + +#: squirrelbattle/settings.py:27 squirrelbattle/tests/translations_test.py:33 +#: squirrelbattle/tests/translations_test.py:37 +#: squirrelbattle/tests/translations_test.py:39 +msgid "Main key to move right" +msgstr "" + +#: squirrelbattle/settings.py:29 squirrelbattle/tests/translations_test.py:35 +#: squirrelbattle/tests/translations_test.py:39 +#: squirrelbattle/tests/translations_test.py:41 +msgid "Secondary key to move right" +msgstr "" + +#: squirrelbattle/settings.py:30 squirrelbattle/tests/translations_test.py:37 +#: squirrelbattle/tests/translations_test.py:41 +#: squirrelbattle/tests/translations_test.py:43 +msgid "Key to validate a menu" +msgstr "" + +#: squirrelbattle/settings.py:31 squirrelbattle/tests/translations_test.py:39 +#: squirrelbattle/tests/translations_test.py:43 +#: squirrelbattle/tests/translations_test.py:45 +msgid "Texture pack" +msgstr "" + +#: squirrelbattle/settings.py:32 squirrelbattle/tests/translations_test.py:40 +#: squirrelbattle/tests/translations_test.py:44 +#: squirrelbattle/tests/translations_test.py:46 +msgid "Language" +msgstr "" + +#: squirrelbattle/interfaces.py:407 squirrelbattle/interfaces.py:412 +#: squirrelbattle/interfaces.py:422 +#, python-brace-format +msgid "{name} dies." +msgstr "" + +#: squirrelbattle/tests/translations_test.py:47 +#: squirrelbattle/tests/translations_test.py:49 +msgid "player" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:49 +#: squirrelbattle/tests/translations_test.py:51 +msgid "tiger" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:50 +#: squirrelbattle/tests/translations_test.py:52 +msgid "hedgehog" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:51 +#: squirrelbattle/tests/translations_test.py:53 +msgid "rabbit" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:52 +#: squirrelbattle/tests/translations_test.py:54 +msgid "teddy bear" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:54 +#: squirrelbattle/tests/translations_test.py:56 +msgid "bomb" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:55 +#: squirrelbattle/tests/translations_test.py:57 +msgid "heart" +msgstr "" + +#: squirrelbattle/entities/friendly.py:31 +msgid "Flower power!!" +msgstr "" + +#: squirrelbattle/entities/friendly.py:31 +msgid "The sun is warm today" +msgstr "" diff --git a/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po index acbfb5a..f4d1c3c 100644 --- a/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po @@ -1,49 +1,67 @@ -# Spanish translation of Squirrel Battle -# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao # This file is distributed under the same license as the squirrelbattle package. -# Translation by ifugaao +# FIRST AUTHOR , YEAR. # +#, fuzzy msgid "" msgstr "" "Project-Id-Version: squirrelbattle 3.14.1\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" -"POT-Creation-Date: 2020-12-05 14:46+0100\n" +"POT-Creation-Date: 2020-12-12 18:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: ifugao\n" +"Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -# Suggested in Weblate: == INVENTORIO == -#: squirrelbattle/display/menudisplay.py:105 -msgid "== INVENTORY ==" -msgstr "== INVENTORIO ==" +#: squirrelbattle/display/menudisplay.py:139 +msgid "INVENTORY" +msgstr "INVENTORIO" -# Suggested in Weblate: Inventorio : -#: squirrelbattle/display/statsdisplay.py:34 +#: squirrelbattle/display/menudisplay.py:164 +msgid "STALL" +msgstr "PUESTO" + +#: squirrelbattle/display/statsdisplay.py:33 msgid "Inventory:" msgstr "Inventorio :" -# Suggested in Weblate: ERES MUERTO -#: squirrelbattle/display/statsdisplay.py:50 +#: squirrelbattle/display/statsdisplay.py:52 msgid "YOU ARE DEAD" msgstr "ERES MUERTO" +#: squirrelbattle/entities/friendly.py:33 +msgid "I don't sell any squirrel" +msgstr "No vendo ninguna ardilla" + +#: squirrelbattle/entities/friendly.py:52 +msgid "Flower power!!" +msgstr "Poder de las flores!!" + +#: squirrelbattle/entities/friendly.py:52 +msgid "The sun is warm today" +msgstr "El sol está caliente hoy" + #. The bomb is exploding. #. Each entity that is close to the bomb takes damages. #. The player earn XP if the entity was killed. -#: squirrelbattle/entities/items.py:128 +#: squirrelbattle/entities/items.py:151 msgid "Bomb is exploding." msgstr "La bomba está explotando." -#: squirrelbattle/entities/items.py:172 +#: squirrelbattle/entities/items.py:248 #, python-brace-format msgid "{player} exchanged its body with {entity}." msgstr "{player} intercambió su cuerpo con {entity}." -#: squirrelbattle/game.py:177 +#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 +msgid "You do not have enough money" +msgstr "No tienes suficiente dinero" + +#: squirrelbattle/game.py:249 msgid "" "Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted." @@ -51,7 +69,7 @@ msgstr "" "Algunas claves faltan en su archivo de guarda.\n" "Su guarda parece a ser corruptido. Fue eliminado." -#: squirrelbattle/game.py:185 +#: squirrelbattle/game.py:257 msgid "" "No player was found on this map!\n" "Maybe you died?" @@ -59,7 +77,7 @@ msgstr "" "No jugador encontrado sobre la carta !\n" "¿ Quizas murió ?" -#: squirrelbattle/game.py:205 +#: squirrelbattle/game.py:277 msgid "" "The JSON file is not correct.\n" "Your save seems corrupted. It got deleted." @@ -67,28 +85,32 @@ msgstr "" "El JSON archivo no es correcto.\n" "Su guarda parece corrupta. Fue eliminada." -#: squirrelbattle/interfaces.py:400 +#: squirrelbattle/interfaces.py:429 #, python-brace-format msgid "{name} hits {opponent}." msgstr "{name} golpea a {opponent}." -#: squirrelbattle/interfaces.py:412 +#: squirrelbattle/interfaces.py:441 #, python-brace-format msgid "{name} takes {amount} damage." msgstr "{name} recibe {amount} daño." -#: squirrelbattle/interfaces.py:414 +#: squirrelbattle/interfaces.py:443 #, python-brace-format msgid "{name} dies." msgstr "{name} se muere." -#: squirrelbattle/menus.py:72 +#: squirrelbattle/interfaces.py:477 +#, python-brace-format +msgid "{entity} said: {message}" +msgstr "{entity} dijo : {message}" + +#: squirrelbattle/menus.py:73 msgid "Back" msgstr "Volver" -#: squirrelbattle/tests/game_test.py:300, -#: squirrelbattle/tests/game_test.py:303, -#: squirrelbattle/tests/game_test.py:306, +#: squirrelbattle/tests/game_test.py:344 squirrelbattle/tests/game_test.py:347 +#: squirrelbattle/tests/game_test.py:350 squirrelbattle/tests/game_test.py:353 #: squirrelbattle/tests/translations_test.py:16 msgid "New game" msgstr "Nuevo partido" @@ -166,41 +188,65 @@ msgid "Key used to drop an item in the inventory" msgstr "Tecla para dejar un objeto del inventorio" #: squirrelbattle/tests/translations_test.py:53 +msgid "Key used to talk to a friendly entity" +msgstr "Tecla para hablar con una entidad amiga" + +#: squirrelbattle/tests/translations_test.py:55 +msgid "Key used to wait" +msgstr "Tecla para espera" + +#: squirrelbattle/tests/translations_test.py:56 msgid "Texture pack" msgstr "Paquete de texturas" -#: squirrelbattle/tests/translations_test.py:54 +#: squirrelbattle/tests/translations_test.py:57 msgid "Language" msgstr "Languaje" -#: squirrelbattle/tests/translations_test.py:57 +#: squirrelbattle/tests/translations_test.py:60 msgid "player" msgstr "jugador" -#: squirrelbattle/tests/translations_test.py:59 -msgid "tiger" -msgstr "tigre" - -#: squirrelbattle/tests/translations_test.py:60 +#: squirrelbattle/tests/translations_test.py:62 msgid "hedgehog" msgstr "erizo" -#: squirrelbattle/tests/translations_test.py:61 +#: squirrelbattle/tests/translations_test.py:63 +msgid "merchant" +msgstr "comerciante" + +#: squirrelbattle/tests/translations_test.py:64 msgid "rabbit" msgstr "conejo" -#: squirrelbattle/tests/translations_test.py:62 +#: squirrelbattle/tests/translations_test.py:65 +msgid "sunflower" +msgstr "girasol" + +#: squirrelbattle/tests/translations_test.py:66 msgid "teddy bear" msgstr "osito de peluche" -#: squirrelbattle/tests/translations_test.py:64 +#: squirrelbattle/tests/translations_test.py:67 +msgid "tiger" +msgstr "tigre" + +#: squirrelbattle/tests/translations_test.py:69 msgid "body snatch potion" msgstr "poción de intercambio" -#: squirrelbattle/tests/translations_test.py:65 +#: squirrelbattle/tests/translations_test.py:70 msgid "bomb" msgstr "bomba" -#: squirrelbattle/tests/translations_test.py:66 +#: squirrelbattle/tests/translations_test.py:71 +msgid "explosion" +msgstr "explosión" + +#: squirrelbattle/tests/translations_test.py:72 msgid "heart" msgstr "corazón" + +#: squirrelbattle/tests/translations_test.py:73 +msgid "sword" +msgstr "espada" diff --git a/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po index 85bd728..8b1c4db 100644 --- a/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po @@ -1,46 +1,68 @@ -# French translation of Squirrel Battle -# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao # This file is distributed under the same license as the squirrelbattle package. +# FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: squirrelbattle 3.14.1\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" -"POT-Creation-Date: 2020-12-05 14:46+0100\n" +"POT-Creation-Date: 2020-12-12 18:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" -"Language: fr\n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: squirrelbattle/display/menudisplay.py:105 -msgid "== INVENTORY ==" -msgstr "== INVENTAIRE ==" +#: squirrelbattle/display/menudisplay.py:139 +msgid "INVENTORY" +msgstr "INVENTAIRE" -#: squirrelbattle/display/statsdisplay.py:34 +#: squirrelbattle/display/menudisplay.py:164 +msgid "STALL" +msgstr "STAND" + +#: squirrelbattle/display/statsdisplay.py:33 msgid "Inventory:" msgstr "Inventaire :" -#: squirrelbattle/display/statsdisplay.py:50 +#: squirrelbattle/display/statsdisplay.py:52 msgid "YOU ARE DEAD" msgstr "VOUS ÊTES MORT" +#. TODO +#: squirrelbattle/entities/friendly.py:33 +msgid "I don't sell any squirrel" +msgstr "Je ne vends pas d'écureuil" + +#: squirrelbattle/entities/friendly.py:52 +msgid "Flower power!!" +msgstr "Pouvoir des fleurs !!" + +#: squirrelbattle/entities/friendly.py:52 +msgid "The sun is warm today" +msgstr "Le soleil est chaud aujourd'hui" + #. The bomb is exploding. #. Each entity that is close to the bomb takes damages. #. The player earn XP if the entity was killed. -#: squirrelbattle/entities/items.py:128 +#: squirrelbattle/entities/items.py:151 msgid "Bomb is exploding." msgstr "La bombe explose." -#: squirrelbattle/entities/items.py:172 +#: squirrelbattle/entities/items.py:248 #, python-brace-format msgid "{player} exchanged its body with {entity}." msgstr "{player} a échangé son corps avec {entity}." -#: squirrelbattle/game.py:177 +#: 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" + +#: squirrelbattle/game.py:249 msgid "" "Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted." @@ -48,7 +70,7 @@ msgstr "" "Certaines clés de votre ficher de sauvegarde sont manquantes.\n" "Votre sauvegarde semble corrompue. Elle a été supprimée." -#: squirrelbattle/game.py:185 +#: squirrelbattle/game.py:257 msgid "" "No player was found on this map!\n" "Maybe you died?" @@ -56,7 +78,7 @@ msgstr "" "Aucun joueur n'a été trouvé sur la carte !\n" "Peut-être êtes-vous mort ?" -#: squirrelbattle/game.py:205 +#: squirrelbattle/game.py:277 msgid "" "The JSON file is not correct.\n" "Your save seems corrupted. It got deleted." @@ -64,27 +86,32 @@ msgstr "" "Le fichier JSON de sauvegarde est incorrect.\n" "Votre sauvegarde semble corrompue. Elle a été supprimée." -#: squirrelbattle/interfaces.py:400 +#: squirrelbattle/interfaces.py:429 #, python-brace-format msgid "{name} hits {opponent}." msgstr "{name} frappe {opponent}." -#: squirrelbattle/interfaces.py:412 +#: squirrelbattle/interfaces.py:441 #, python-brace-format msgid "{name} takes {amount} damage." msgstr "{name} prend {amount} points de dégât." -#: squirrelbattle/interfaces.py:414 +#: squirrelbattle/interfaces.py:443 #, python-brace-format msgid "{name} dies." msgstr "{name} meurt." -#: squirrelbattle/menus.py:72 +#: squirrelbattle/interfaces.py:477 +#, python-brace-format +msgid "{entity} said: {message}" +msgstr "{entity} a dit : {message}" + +#: squirrelbattle/menus.py:73 msgid "Back" msgstr "Retour" -#: squirrelbattle/tests/game_test.py:300 squirrelbattle/tests/game_test.py:303 -#: squirrelbattle/tests/game_test.py:306 +#: squirrelbattle/tests/game_test.py:344 squirrelbattle/tests/game_test.py:347 +#: squirrelbattle/tests/game_test.py:350 squirrelbattle/tests/game_test.py:353 #: squirrelbattle/tests/translations_test.py:16 msgid "New game" msgstr "Nouvelle partie" @@ -162,41 +189,65 @@ msgid "Key used to drop an item in the inventory" msgstr "Touche pour jeter un objet de l'inventaire" #: squirrelbattle/tests/translations_test.py:53 +msgid "Key used to talk to a friendly entity" +msgstr "Touche pour parler à une entité pacifique" + +#: squirrelbattle/tests/translations_test.py:55 +msgid "Key used to wait" +msgstr "Touche pour attendre" + +#: squirrelbattle/tests/translations_test.py:56 msgid "Texture pack" msgstr "Pack de textures" -#: squirrelbattle/tests/translations_test.py:54 +#: squirrelbattle/tests/translations_test.py:57 msgid "Language" msgstr "Langue" -#: squirrelbattle/tests/translations_test.py:57 +#: squirrelbattle/tests/translations_test.py:60 msgid "player" msgstr "joueur" -#: squirrelbattle/tests/translations_test.py:59 -msgid "tiger" -msgstr "tigre" - -#: squirrelbattle/tests/translations_test.py:60 +#: squirrelbattle/tests/translations_test.py:62 msgid "hedgehog" msgstr "hérisson" -#: squirrelbattle/tests/translations_test.py:61 +#: squirrelbattle/tests/translations_test.py:63 +msgid "merchant" +msgstr "marchand" + +#: squirrelbattle/tests/translations_test.py:64 msgid "rabbit" msgstr "lapin" -#: squirrelbattle/tests/translations_test.py:62 +#: squirrelbattle/tests/translations_test.py:65 +msgid "sunflower" +msgstr "tournesol" + +#: squirrelbattle/tests/translations_test.py:66 msgid "teddy bear" msgstr "nounours" -#: squirrelbattle/tests/translations_test.py:64 +#: squirrelbattle/tests/translations_test.py:67 +msgid "tiger" +msgstr "tigre" + +#: squirrelbattle/tests/translations_test.py:69 msgid "body snatch potion" msgstr "potion d'arrachage de corps" -#: squirrelbattle/tests/translations_test.py:65 +#: squirrelbattle/tests/translations_test.py:70 msgid "bomb" msgstr "bombe" -#: squirrelbattle/tests/translations_test.py:66 +#: squirrelbattle/tests/translations_test.py:71 +msgid "explosion" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:72 msgid "heart" msgstr "cœur" + +#: squirrelbattle/tests/translations_test.py:73 +msgid "sword" +msgstr "épée" diff --git a/squirrelbattle/menus.py b/squirrelbattle/menus.py index 3a536f3..e0087a3 100644 --- a/squirrelbattle/menus.py +++ b/squirrelbattle/menus.py @@ -6,6 +6,7 @@ from typing import Any, Optional from .display.texturepack import TexturePack from .entities.player import Player +from .entities.friendly import Merchant from .enums import GameMode, KeyValues, DisplayActions from .settings import Settings from .translations import gettext as _, Translator @@ -128,3 +129,14 @@ class InventoryMenu(Menu): @property def values(self) -> list: return self.player.inventory + + +class StoreMenu(Menu): + merchant: Merchant + + def update_merchant(self, merchant: Merchant) -> None: + self.merchant = merchant + + @property + def values(self) -> list: + return self.merchant.inventory diff --git a/squirrelbattle/settings.py b/squirrelbattle/settings.py index 4004645..91edfa4 100644 --- a/squirrelbattle/settings.py +++ b/squirrelbattle/settings.py @@ -31,6 +31,8 @@ class Settings: self.KEY_USE = ['u', 'Key used to use an item in the inventory'] self.KEY_EQUIP = ['e', 'Key used to equip an item in the inventory'] self.KEY_DROP = ['r', 'Key used to drop an item in the inventory'] + self.KEY_CHAT = ['t', 'Key used to talk to a friendly entity'] + self.KEY_WAIT = ['w', 'Key used to wait'] self.TEXTURE_PACK = ['ascii', 'Texture pack'] self.LOCALE = [locale.getlocale()[0][:2], 'Language'] 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() diff --git a/squirrelbattle/tests/entities_test.py b/squirrelbattle/tests/entities_test.py index 2c72abd..70e3748 100644 --- a/squirrelbattle/tests/entities_test.py +++ b/squirrelbattle/tests/entities_test.py @@ -3,7 +3,8 @@ import unittest -from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item +from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item, \ + Explosion from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear from squirrelbattle.entities.player import Player from squirrelbattle.interfaces import Entity, Map @@ -138,6 +139,20 @@ class TestEntities(unittest.TestCase): self.assertTrue(teddy_bear.dead) bomb_state = item.save_state() self.assertEqual(bomb_state["damage"], item.damage) + explosions = self.map.find_entities(Explosion) + self.assertTrue(explosions) + explosion = explosions[0] + self.assertEqual(explosion.y, item.y) + self.assertEqual(explosion.x, item.x) + + # The player can't hold the explosion + explosion.hold(self.player) + self.assertNotIn(explosion, self.player.inventory) + self.assertFalse(explosion.held) + + # The explosion disappears after one tick + explosion.act(self.map) + self.assertNotIn(explosion, self.map.entities) def test_hearts(self) -> None: """ diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 3a32c95..52aeeaf 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -7,7 +7,8 @@ import unittest from ..bootstrap import Bootstrap from ..display.display import Display from ..display.display_manager import DisplayManager -from ..entities.items import Bomb +from ..entities.friendly import Merchant, Sunflower +from ..entities.items import Bomb, Heart, Sword, Explosion from ..entities.player import Player from ..enums import DisplayActions from ..game import Game, KeyValues, GameMode @@ -34,7 +35,17 @@ class TestGame(unittest.TestCase): """ bomb = Bomb() self.game.map.add_entity(bomb) + sword = Sword() + self.game.map.add_entity(sword) + # Add items in the inventory to check that it is well loaded bomb.hold(self.game.player) + sword.hold(self.game.player) + + # Ensure that merchants can be saved + merchant = Merchant() + merchant.move(3, 6) + self.game.map.add_entity(merchant) + old_state = self.game.save_state() self.game.handle_key_pressed(KeyValues.DOWN) @@ -117,6 +128,9 @@ class TestGame(unittest.TestCase): self.assertEqual(KeyValues.translate_key( self.game.settings.KEY_INVENTORY, self.game.settings), KeyValues.INVENTORY) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_CHAT, self.game.settings), + KeyValues.CHAT) self.assertEqual(KeyValues.translate_key( self.game.settings.KEY_USE, self.game.settings), KeyValues.USE) @@ -126,6 +140,9 @@ class TestGame(unittest.TestCase): self.assertEqual(KeyValues.translate_key( self.game.settings.KEY_DROP, self.game.settings), KeyValues.DROP) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_WAIT, self.game.settings), + KeyValues.WAIT) self.assertEqual(KeyValues.translate_key(' ', self.game.settings), KeyValues.SPACE) self.assertEqual(KeyValues.translate_key('plop', self.game.settings), @@ -213,9 +230,45 @@ class TestGame(unittest.TestCase): self.assertEqual(new_y, y) self.assertEqual(new_x, x - 1) + explosion = Explosion() + self.game.map.add_entity(explosion) + self.assertIn(explosion, self.game.map.entities) + self.game.handle_key_pressed(KeyValues.WAIT) + self.assertNotIn(explosion, self.game.map.entities) + 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 + + # Change the color of the artwork + self.game.display_actions(DisplayActions.MOUSE, 0, 10) + + # 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. @@ -253,13 +306,13 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.ENTER) self.assertEqual(self.game.state, GameMode.SETTINGS) - # Define the "move up" key to 'w' + # Define the "move up" key to 'h' self.assertFalse(self.game.settings_menu.waiting_for_key) self.game.handle_key_pressed(KeyValues.ENTER) self.assertTrue(self.game.settings_menu.waiting_for_key) - self.game.handle_key_pressed(None, 'w') + self.game.handle_key_pressed(None, 'h') self.assertFalse(self.game.settings_menu.waiting_for_key) - self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w') + self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'h') # Navigate to "move left" self.game.handle_key_pressed(KeyValues.DOWN) @@ -280,7 +333,7 @@ class TestGame(unittest.TestCase): self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a') # Navigate to "texture pack" - for ignored in range(9): + for ignored in range(11): self.game.handle_key_pressed(KeyValues.DOWN) # Change texture pack @@ -417,3 +470,118 @@ class TestGame(unittest.TestCase): self.assertTrue(bomb.exploding) self.assertEqual(bomb.y, self.game.player.y) self.assertEqual(bomb.x, self.game.player.x) + + def test_talk_to_sunflowers(self) -> None: + """ + Interact with sunflowers + """ + self.game.state = GameMode.PLAY + + sunflower = Sunflower() + sunflower.move(2, 6) + self.game.map.add_entity(sunflower) + + # Does nothing + self.assertIsNone(self.game.handle_friendly_entity_chat(KeyValues.UP)) + + # Talk to sunflower... or not + self.game.handle_key_pressed(KeyValues.CHAT) + self.assertTrue(self.game.waiting_for_friendly_key) + # Wrong key + self.game.handle_key_pressed(KeyValues.EQUIP) + self.assertFalse(self.game.waiting_for_friendly_key) + self.game.handle_key_pressed(KeyValues.CHAT) + self.assertTrue(self.game.waiting_for_friendly_key) + self.game.handle_key_pressed(KeyValues.UP) + self.assertFalse(self.game.waiting_for_friendly_key) + self.assertEqual(self.game.state, GameMode.PLAY) + self.assertFalse(len(self.game.logs.messages) > 1) + + # Talk to sunflower + self.game.handle_key_pressed(KeyValues.CHAT) + self.assertTrue(self.game.waiting_for_friendly_key) + self.game.handle_key_pressed(KeyValues.DOWN) + self.assertFalse(self.game.waiting_for_friendly_key) + self.assertEqual(self.game.state, GameMode.PLAY) + self.assertTrue(self.game.logs.messages) + # Ensure that the message is a good message + self.assertTrue(any(self.game.logs.messages[1].endswith(msg) + for msg in Sunflower().dialogue_option)) + + # Test all directions to detect the friendly entity + self.game.player.move(3, 6) + self.game.handle_key_pressed(KeyValues.CHAT) + self.game.handle_key_pressed(KeyValues.UP) + self.assertEqual(len(self.game.logs.messages), 3) + self.game.player.move(2, 7) + self.game.handle_key_pressed(KeyValues.CHAT) + self.game.handle_key_pressed(KeyValues.LEFT) + self.assertEqual(len(self.game.logs.messages), 4) + self.game.player.move(2, 5) + self.game.handle_key_pressed(KeyValues.CHAT) + self.game.handle_key_pressed(KeyValues.RIGHT) + self.assertEqual(len(self.game.logs.messages), 5) + + def test_talk_to_merchant(self) -> None: + """ + Interact with merchants + """ + self.game.state = GameMode.PLAY + + merchant = Merchant() + merchant.move(2, 6) + self.game.map.add_entity(merchant) + + # Does nothing + self.assertIsNone(self.game.handle_friendly_entity_chat(KeyValues.UP)) + + # Talk to merchant + self.game.handle_key_pressed(KeyValues.CHAT) + self.assertTrue(self.game.waiting_for_friendly_key) + self.game.handle_key_pressed(KeyValues.DOWN) + self.assertFalse(self.game.waiting_for_friendly_key) + self.assertEqual(self.game.state, GameMode.STORE) + self.assertTrue(self.game.logs.messages) + + # Navigate in the menu + self.game.handle_key_pressed(KeyValues.DOWN) + self.game.handle_key_pressed(KeyValues.DOWN) + 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() + # 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) + + # Buy a heart + merchant.inventory[1] = Heart() + item = self.game.store_menu.validate() + self.assertIn(item, merchant.inventory) + self.assertEqual(item, merchant.inventory[1]) + self.game.player.health = self.game.player.maxhealth - 1 - item.healing + self.game.handle_key_pressed(KeyValues.ENTER) + self.assertNotIn(item, self.game.player.inventory) + self.assertNotIn(item, merchant.inventory) + self.assertEqual(self.game.player.health, + self.game.player.maxhealth - 1) + + # We don't have enough of money + self.game.player.hazel = 0 + item = self.game.store_menu.validate() + 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.game.handle_key_pressed(KeyValues.ENTER) + + # Exit the menu + self.game.handle_key_pressed(KeyValues.SPACE) + self.assertEqual(self.game.state, GameMode.PLAY) diff --git a/squirrelbattle/tests/translations_test.py b/squirrelbattle/tests/translations_test.py index 0cb39c5..0bd8873 100644 --- a/squirrelbattle/tests/translations_test.py +++ b/squirrelbattle/tests/translations_test.py @@ -50,17 +50,24 @@ class TestTranslations(unittest.TestCase): "Touche pour équiper un objet de l'inventaire") self.assertEqual(_("Key used to drop an item in the inventory"), "Touche pour jeter un objet de l'inventaire") + self.assertEqual(_("Key used to talk to a friendly entity"), + "Touche pour parler à une entité pacifique") + self.assertEqual(_("Key used to wait"), "Touche pour attendre") self.assertEqual(_("Texture pack"), "Pack de textures") self.assertEqual(_("Language"), "Langue") def test_entities_translation(self) -> None: self.assertEqual(_("player"), "joueur") - self.assertEqual(_("tiger"), "tigre") self.assertEqual(_("hedgehog"), "hérisson") + self.assertEqual(_("merchant"), "marchand") self.assertEqual(_("rabbit"), "lapin") + self.assertEqual(_("sunflower"), "tournesol") self.assertEqual(_("teddy bear"), "nounours") + self.assertEqual(_("tiger"), "tigre") self.assertEqual(_("body snatch potion"), "potion d'arrachage de corps") self.assertEqual(_("bomb"), "bombe") + self.assertEqual(_("explosion"), "explosion") self.assertEqual(_("heart"), "cœur") + self.assertEqual(_("sword"), "épée")