Merge branch 'master' into map_generation
# Conflicts: # squirrelbattle/game.py # squirrelbattle/interfaces.py # squirrelbattle/tests/game_test.py
This commit is contained in:
		
							
								
								
									
										44
									
								
								squirrelbattle/assets/ascii-art-ecureuil.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								squirrelbattle/assets/ascii-art-ecureuil.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
                                                                              
 | 
			
		||||
                  ⋀
 | 
			
		||||
                 ┃|┃
 | 
			
		||||
                 ┃|┃        ▓▓▒            ▓▓
 | 
			
		||||
                 ┃|┃         ▓▓           ▓▓▒
 | 
			
		||||
                 ┃|┃         ▓▓▓    ▓▓   ▓▓▓            ▒▒▒▒▒▒▒▒▒
 | 
			
		||||
                 ┃|┃          ▓▓▓▓▓▓▓▓▓▓▓▓▓           ▒▒▒▒▒▒▒▒▒▒▒▒▒
 | 
			
		||||
                 ┃|┃          ▓▓▓▓▓▓▓▓▓▓▓▓▓         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 | 
			
		||||
                 ┃|┃         ▓▓▓▬█▓▓▓▓▓▓▬█▓▓▓      ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 | 
			
		||||
                 ┃|┃       ▓▓▓▓░██░░▓▓░░██░▓▓▓▓   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 | 
			
		||||
               ━━▓▓▓▓━━   ▓▓░░░░░░░░  ░░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 | 
			
		||||
                  ▓▓▓▓▓▓  ▓░░░░░░░░░░░░░░░░░░░░▓▓▒▒▒▒▒▒▒▒▒▒▒▒                 
 | 
			
		||||
                  ┃ ▓▓▓▓▓  ▓░░░░░░░░▄▄▄▄░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▒
 | 
			
		||||
                  ┃  ▓▓▓▓▓ ▓▓░░░░░░░░░░░░░░░▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 | 
			
		||||
                       ▓▓▓▓   ▓▓▓▓░░░░░░░▓▓   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒               
 | 
			
		||||
                        ▓▓▓▓▓▓▒▒░░░░░░░░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒               
 | 
			
		||||
                         ▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒               
 | 
			
		||||
                          ▓▓▒░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒              
 | 
			
		||||
                          ▓▒▒░░░░░░░░░░░░▓▓▓▓▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒              
 | 
			
		||||
                         ▓▓▒░░░░░░░░░░░░░░░▓▒▒▒▒▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒              
 | 
			
		||||
                         ▓▓▒▒░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒               
 | 
			
		||||
                         ▓▓▓▒░░░░░░░░░░░░░▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒                
 | 
			
		||||
                       ▓▓▒▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒                 
 | 
			
		||||
                    ▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒                   
 | 
			
		||||
                  ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒                     
 | 
			
		||||
                  ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒                      
 | 
			
		||||
                 ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒                        
 | 
			
		||||
                 ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓                          
 | 
			
		||||
                 ▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓                          
 | 
			
		||||
                  ▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓     ░                     
 | 
			
		||||
                   ▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓    ░░                      
 | 
			
		||||
                ▓▓▓▓▓▓▓▓▒▒░░░▒▒▒▒░░░░░░▓▓░▒▒▒▓▓▓▓▓▓▓▓▓▓░░░   ░                
 | 
			
		||||
               ▓▓▓▓▓▓▓▒░░░░░░░░░▒░░░░░░░░░░░░▒▒▒▓▓▓▓▓▓▓▓░░ ░░▒                
 | 
			
		||||
             ░ ░░░░░▒░░░░░░▒░░░▒░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░▒               
 | 
			
		||||
           ▒▒░░▓▓░░▒░░░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░░░░░▒░░░░░▒   ░░           
 | 
			
		||||
         ▒▒▒▒▒▓▒▒▓░░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░           
 | 
			
		||||
        ▒▒█▒█▒▒▒▓░░▒░░░░░░░░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░
 | 
			
		||||
       ▒▒▒▒█▒▒▒▒░░░░▒░░░▒░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░▒░░░
 | 
			
		||||
    ▓█▒▒▒▒█▒█▒▒▒▒░░▒░░░░░▒░░░░▒░░░░░░░░░░░░░░░░░▒░░░░▒░░░░░░░▒░░░░░▒▒
 | 
			
		||||
    ██▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░▒░░░░░░▒░░░░░▒░░░░░            
 | 
			
		||||
        ▒▒▒▒█▒▒▒▒▒▒▒░░░░░░░░░░▒░░░░░░░░░░▒░░░░░░░░░░░▒░░░░░░░░░░░░░░░
 | 
			
		||||
         ▒▒█▒▒▒▒▒░▒░▒░░░░▓▓▓░░░░░░░▒░░░░▒░░░▒░░░░░░░▓▓░░░░░░░░░░░░  ░
 | 
			
		||||
           ▒▒▒▒▒▒▒░▒░░░▓▓▓▓▓▓░░░░░░░▒░░░░░░░░▒░░░░▓▓▓▓▓▓░░░░░░░░  ░           
 | 
			
		||||
                      ░▓▓▓▓▓▓░░░░░░▒░░░░░░░░▒░░░░░░▓▓▓▓▓░░░ ░  ░░             
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
1 6
 | 
			
		||||
    #######                    #############        
 | 
			
		||||
    #.....#                    #...........#        
 | 
			
		||||
    #.H...#                    #...........#        
 | 
			
		||||
    #.....#                #####...........#        
 | 
			
		||||
    #.....#                #...............#        
 | 
			
		||||
    #.....#                #............H..#        
 | 
			
		||||
    #.#####                #.###...........#        
 | 
			
		||||
    #.#                    #.# #...........#        
 | 
			
		||||
    #.#                    #.# #############        
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
1 17
 | 
			
		||||
            ###########                        #########                        
 | 
			
		||||
            #.........#                        #.......#                        
 | 
			
		||||
            #....H....#                        #.......#                        
 | 
			
		||||
            #.........#             ############.......#                        
 | 
			
		||||
            #.........###############..........#.......##############           
 | 
			
		||||
            #.........#........................#....................#           
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
      ########.##########......#     #.........#              #.........#       
 | 
			
		||||
           #...........##......#     #.........#              #.........#       
 | 
			
		||||
           #...........##......#     #.........#              #.........#       
 | 
			
		||||
           #...........##......#     #.........#  ################.######       
 | 
			
		||||
           #...........##..H...#     #.........#  ################.######       
 | 
			
		||||
           #...........##......#     #.........#  #.................############
 | 
			
		||||
           #...........##......#  ########.########.......#.........#..........#
 | 
			
		||||
           #...........##......#  #...............#.......#.........#..........#
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								squirrelbattle/assets/example_map_3.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								squirrelbattle/assets/example_map_3.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
1 6
 | 
			
		||||
################################################################################
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..#...........................................................................#
 | 
			
		||||
#...........#..................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
#..............................................................................#
 | 
			
		||||
################################################################################
 | 
			
		||||
							
								
								
									
										97
									
								
								squirrelbattle/display/creditsdisplay.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								squirrelbattle/display/creditsdisplay.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
import curses
 | 
			
		||||
 | 
			
		||||
from ..display.display import Box, Display
 | 
			
		||||
from ..game import Game
 | 
			
		||||
from ..resources import ResourceManager
 | 
			
		||||
from ..translations import gettext as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreditsDisplay(Display):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.box = Box(*args, **kwargs)
 | 
			
		||||
        self.pad = self.newpad(1, 1)
 | 
			
		||||
        self.ascii_art_displayed = False
 | 
			
		||||
 | 
			
		||||
    def update(self, game: Game) -> None:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def display(self) -> None:
 | 
			
		||||
        self.box.refresh(self.y, self.x, self.height, self.width)
 | 
			
		||||
        self.box.display()
 | 
			
		||||
        self.pad.erase()
 | 
			
		||||
 | 
			
		||||
        messages = [
 | 
			
		||||
            _("Credits"),
 | 
			
		||||
            "",
 | 
			
		||||
            "Squirrel Battle",
 | 
			
		||||
            "",
 | 
			
		||||
            _("Developers:"),
 | 
			
		||||
            "Yohann \"ÿnérant\" D'ANELLO",
 | 
			
		||||
            "Mathilde \"eichhornchen\" DÉPRÉS",
 | 
			
		||||
            "Nicolas \"nicomarg\" MARGULIES",
 | 
			
		||||
            "Charles \"charsle\" PEYRAT",
 | 
			
		||||
            "",
 | 
			
		||||
            _("Translators:"),
 | 
			
		||||
            "Hugo \"ifugao\" JACOB (español)",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for i, msg in enumerate(messages):
 | 
			
		||||
            self.addstr(self.pad, i + (self.height - len(messages)) // 2,
 | 
			
		||||
                        (self.width - len(msg)) // 2, msg,
 | 
			
		||||
                        bold=(i == 0), italic=(":" in msg))
 | 
			
		||||
 | 
			
		||||
        if self.ascii_art_displayed:
 | 
			
		||||
            self.display_ascii_art()
 | 
			
		||||
 | 
			
		||||
        self.refresh_pad(self.pad, 0, 0, self.y + 1, self.x + 1,
 | 
			
		||||
                         self.height + self.y - 2,
 | 
			
		||||
                         self.width + self.x - 2)
 | 
			
		||||
 | 
			
		||||
    def display_ascii_art(self) -> None:
 | 
			
		||||
        with open(ResourceManager.get_asset_path("ascii-art-ecureuil.txt"))\
 | 
			
		||||
                as f:
 | 
			
		||||
            ascii_art = f.read().split("\n")
 | 
			
		||||
 | 
			
		||||
        height, width = len(ascii_art), len(ascii_art[0])
 | 
			
		||||
        y_offset, x_offset = (self.height - height) // 2,\
 | 
			
		||||
                             (self.width - width) // 2
 | 
			
		||||
 | 
			
		||||
        for i, line in enumerate(ascii_art):
 | 
			
		||||
            for j, c in enumerate(line):
 | 
			
		||||
                bg_color = curses.COLOR_WHITE
 | 
			
		||||
                fg_color = curses.COLOR_BLACK
 | 
			
		||||
                bold = False
 | 
			
		||||
                if c == ' ':
 | 
			
		||||
                    bg_color = curses.COLOR_BLACK
 | 
			
		||||
                elif c == '━' or c == '┃' or c == '⋀':
 | 
			
		||||
                    bold = True
 | 
			
		||||
                    fg_color = curses.COLOR_WHITE
 | 
			
		||||
                    bg_color = curses.COLOR_BLACK
 | 
			
		||||
                elif c == '|':
 | 
			
		||||
                    bold = True  # c = '┃'
 | 
			
		||||
                    fg_color = (100, 700, 1000)
 | 
			
		||||
                    bg_color = curses.COLOR_BLACK
 | 
			
		||||
                elif c == '▓':
 | 
			
		||||
                    fg_color = (700, 300, 0)
 | 
			
		||||
                elif c == '▒':
 | 
			
		||||
                    fg_color = (700, 300, 0)
 | 
			
		||||
                    bg_color = curses.COLOR_BLACK
 | 
			
		||||
                elif c == '░':
 | 
			
		||||
                    fg_color = (350, 150, 0)
 | 
			
		||||
                elif c == '█':
 | 
			
		||||
                    fg_color = (0, 0, 0)
 | 
			
		||||
                    bg_color = curses.COLOR_BLACK
 | 
			
		||||
                elif c == '▬':
 | 
			
		||||
                    c = '█'
 | 
			
		||||
                    fg_color = (1000, 1000, 1000)
 | 
			
		||||
                    bg_color = curses.COLOR_BLACK
 | 
			
		||||
                self.addstr(self.pad, y_offset + i, x_offset + j, c,
 | 
			
		||||
                            fg_color, bg_color, bold=bold)
 | 
			
		||||
 | 
			
		||||
    def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
 | 
			
		||||
        if self.pad.inch(y - 1, x - 1) != ord(" "):
 | 
			
		||||
            self.ascii_art_displayed = True
 | 
			
		||||
@@ -2,7 +2,8 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
import curses
 | 
			
		||||
from typing import Any, Optional, Union
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Any, Optional, Tuple, Union
 | 
			
		||||
 | 
			
		||||
from squirrelbattle.display.texturepack import TexturePack
 | 
			
		||||
from squirrelbattle.game import Game
 | 
			
		||||
@@ -16,14 +17,24 @@ 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")
 | 
			
		||||
 | 
			
		||||
    def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
 | 
			
		||||
        """
 | 
			
		||||
        Overwrites the native curses function of the same name.
 | 
			
		||||
        """
 | 
			
		||||
        return curses.newpad(height, width) if self.screen else FakePad()
 | 
			
		||||
 | 
			
		||||
    def truncate(self, msg: str, height: int, width: int) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Truncates a string into a string adapted to the width and height of
 | 
			
		||||
        the screen.
 | 
			
		||||
        """
 | 
			
		||||
        height = max(0, height)
 | 
			
		||||
        width = max(0, width)
 | 
			
		||||
        lines = msg.split("\n")
 | 
			
		||||
@@ -31,15 +42,86 @@ 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:
 | 
			
		||||
        """
 | 
			
		||||
        Display a message onto the pad.
 | 
			
		||||
        Translates a tuple (R, G, B) into a curses color index.
 | 
			
		||||
        If we already have 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:
 | 
			
		||||
        """
 | 
			
		||||
        Displays 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 right 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
 | 
			
		||||
            # Italic is supported since Python 3.7
 | 
			
		||||
            italic &= sys.version_info >= (3, 7,)
 | 
			
		||||
            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) \
 | 
			
		||||
@@ -48,17 +130,28 @@ 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:
 | 
			
		||||
        """
 | 
			
		||||
        Resizes a pad.
 | 
			
		||||
        """
 | 
			
		||||
        self.x = x
 | 
			
		||||
        self.y = y
 | 
			
		||||
        self.width = width
 | 
			
		||||
        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:
 | 
			
		||||
        """
 | 
			
		||||
        Refreshes a pad
 | 
			
		||||
        """
 | 
			
		||||
        if len(args) == 4:
 | 
			
		||||
            self.resize(*args, resize_pad)
 | 
			
		||||
        self.display()
 | 
			
		||||
@@ -67,10 +160,10 @@ class Display:
 | 
			
		||||
                    window_y: int, window_x: int,
 | 
			
		||||
                    last_y: int, last_x: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Refresh a pad on a part of the window.
 | 
			
		||||
        Refreshes a pad on a part of the window.
 | 
			
		||||
        The refresh starts at coordinates (top_y, top_x) from the pad,
 | 
			
		||||
        and is drawn from (window_y, window_x) to (last_y, last_x).
 | 
			
		||||
        If coordinates are invalid (negative indexes/length..., then nothing
 | 
			
		||||
        If coordinates are invalid (negative indexes/length...), then nothing
 | 
			
		||||
        is drawn and no error is raised.
 | 
			
		||||
        """
 | 
			
		||||
        top_y, top_x = max(0, top_y), max(0, top_x)
 | 
			
		||||
@@ -82,17 +175,26 @@ 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 handle_click(self, y: int, x: int, game: Game) -> None:
 | 
			
		||||
    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, attr: int, game: Game) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        A mouse click was performed on the coordinates (y, x) of the pad.
 | 
			
		||||
        Maybe it can do something.
 | 
			
		||||
        Maybe it should do something.
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def rows(self) -> int:
 | 
			
		||||
@@ -104,7 +206,9 @@ class Display:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VerticalSplit(Display):
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    A class to split the screen in two vertically with a pretty line.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.pad = self.newpad(self.rows, 1)
 | 
			
		||||
@@ -125,7 +229,9 @@ class VerticalSplit(Display):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HorizontalSplit(Display):
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    A class to split the screen in two horizontally with a pretty line.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.pad = self.newpad(1, self.cols)
 | 
			
		||||
@@ -146,23 +252,32 @@ class HorizontalSplit(Display):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Box(Display):
 | 
			
		||||
    """
 | 
			
		||||
    A class for pretty boxes to print menus and other content.
 | 
			
		||||
    """
 | 
			
		||||
    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)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
import curses
 | 
			
		||||
 | 
			
		||||
from squirrelbattle.display.creditsdisplay import CreditsDisplay
 | 
			
		||||
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit, \
 | 
			
		||||
    Display
 | 
			
		||||
from squirrelbattle.display.mapdisplay import MapDisplay
 | 
			
		||||
@@ -30,17 +32,21 @@ class DisplayManager:
 | 
			
		||||
        self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
 | 
			
		||||
                                               screen, pack)
 | 
			
		||||
        self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
 | 
			
		||||
        self.messagedisplay = MessageDisplay(screen=screen, pack=None)
 | 
			
		||||
        self.messagedisplay = MessageDisplay(screen, pack)
 | 
			
		||||
        self.hbar = HorizontalSplit(screen, pack)
 | 
			
		||||
        self.vbar = VerticalSplit(screen, pack)
 | 
			
		||||
        self.creditsdisplay = CreditsDisplay(screen, pack)
 | 
			
		||||
        self.displays = [self.statsdisplay, self.mapdisplay,
 | 
			
		||||
                         self.mainmenudisplay, self.settingsmenudisplay,
 | 
			
		||||
                         self.logsdisplay, self.messagedisplay,
 | 
			
		||||
                         self.playerinventorydisplay,
 | 
			
		||||
                         self.storeinventorydisplay]
 | 
			
		||||
                         self.storeinventorydisplay, self.creditsdisplay]
 | 
			
		||||
        self.update_game_components()
 | 
			
		||||
 | 
			
		||||
    def handle_display_action(self, action: DisplayActions, *params) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Handles the differents values of display action.
 | 
			
		||||
        """
 | 
			
		||||
        if action == DisplayActions.REFRESH:
 | 
			
		||||
            self.refresh()
 | 
			
		||||
        elif action == DisplayActions.UPDATE:
 | 
			
		||||
@@ -49,19 +55,18 @@ class DisplayManager:
 | 
			
		||||
            self.handle_mouse_click(*params)
 | 
			
		||||
 | 
			
		||||
    def update_game_components(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        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:
 | 
			
		||||
    def handle_mouse_click(self, y: int, x: int, attr: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Handles the mouse clicks.
 | 
			
		||||
        """
 | 
			
		||||
        displays = self.refresh()
 | 
			
		||||
        display = None
 | 
			
		||||
        for d in displays:
 | 
			
		||||
@@ -71,10 +76,14 @@ class DisplayManager:
 | 
			
		||||
                # of that display
 | 
			
		||||
                display = d
 | 
			
		||||
        if display:
 | 
			
		||||
            display.handle_click(y - display.y, x - display.x, self.game)
 | 
			
		||||
            display.handle_click(y - display.y, x - display.x, attr, self.game)
 | 
			
		||||
 | 
			
		||||
    def refresh(self) -> List[Display]:
 | 
			
		||||
        """
 | 
			
		||||
        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 \
 | 
			
		||||
@@ -97,27 +106,41 @@ 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)
 | 
			
		||||
        elif self.game.state == GameMode.SETTINGS:
 | 
			
		||||
            self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
 | 
			
		||||
            displays.append(self.settingsmenudisplay)
 | 
			
		||||
        elif self.game.state == GameMode.CREDITS:
 | 
			
		||||
            self.creditsdisplay.refresh(0, 0, self.rows, self.cols)
 | 
			
		||||
            displays.append(self.creditsdisplay)
 | 
			
		||||
 | 
			
		||||
        if self.game.message:
 | 
			
		||||
            height, width = 0, 0
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
@@ -127,7 +150,7 @@ class DisplayManager:
 | 
			
		||||
 | 
			
		||||
    def resize_window(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        If the window got resized, ensure that the screen size got updated.
 | 
			
		||||
        When the window is resized, ensures that the screen size is updated.
 | 
			
		||||
        """
 | 
			
		||||
        y, x = self.screen.getmaxyx() if self.screen else (0, 0)
 | 
			
		||||
        if self.screen and curses.is_term_resized(self.rows,
 | 
			
		||||
@@ -138,8 +161,16 @@ class DisplayManager:
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def rows(self) -> int:
 | 
			
		||||
        """
 | 
			
		||||
        Overwrites the native curses attribute of the same name,
 | 
			
		||||
        for testing purposes.
 | 
			
		||||
        """
 | 
			
		||||
        return curses.LINES if self.screen else 42
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def cols(self) -> int:
 | 
			
		||||
        """
 | 
			
		||||
        Overwrites the native curses attribute of the same name,
 | 
			
		||||
        for testing purposes.
 | 
			
		||||
        """
 | 
			
		||||
        return curses.COLS if self.screen else 42
 | 
			
		||||
 
 | 
			
		||||
@@ -2,17 +2,23 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from squirrelbattle.display.display import Display
 | 
			
		||||
from squirrelbattle.game import Game
 | 
			
		||||
from squirrelbattle.interfaces import Logs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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:]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,27 +3,42 @@
 | 
			
		||||
 | 
			
		||||
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.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))
 | 
			
		||||
        for j in range(len(self.map.tiles)):
 | 
			
		||||
            for i in range(len(self.map.tiles[j])):
 | 
			
		||||
                if not self.map.seen_tiles[j][i]:
 | 
			
		||||
                    continue
 | 
			
		||||
                fg, bg = self.map.tiles[j][i].visible_color(self.pack) if \
 | 
			
		||||
                    self.map.visibility[j][i] else \
 | 
			
		||||
                    self.map.tiles[j][i].hidden_color(self.pack)
 | 
			
		||||
                self.addstr(self.pad, j, self.pack.tile_width * i,
 | 
			
		||||
                            self.map.tiles[j][i].char(self.pack), fg, bg)
 | 
			
		||||
        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))
 | 
			
		||||
            if self.map.visibility[e.y][e.x]:
 | 
			
		||||
                self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
 | 
			
		||||
                            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 +57,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
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,13 @@
 | 
			
		||||
# 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 ..enums import KeyValues
 | 
			
		||||
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
 | 
			
		||||
from ..translations import gettext as _
 | 
			
		||||
@@ -14,8 +16,9 @@ from ..translations import gettext as _
 | 
			
		||||
 | 
			
		||||
class MenuDisplay(Display):
 | 
			
		||||
    """
 | 
			
		||||
    A class to display the menu objects
 | 
			
		||||
    A class to display the menu objects.
 | 
			
		||||
    """
 | 
			
		||||
    menu: Menu
 | 
			
		||||
    position: int
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
@@ -30,9 +33,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 \
 | 
			
		||||
@@ -43,11 +46,11 @@ 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:
 | 
			
		||||
    def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        We can select a menu item with the mouse.
 | 
			
		||||
        """
 | 
			
		||||
@@ -77,8 +80,13 @@ class MenuDisplay(Display):
 | 
			
		||||
 | 
			
		||||
class SettingsMenuDisplay(MenuDisplay):
 | 
			
		||||
    """
 | 
			
		||||
    A class to display specifically a settingsmenu object
 | 
			
		||||
    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]) + (" : "
 | 
			
		||||
@@ -90,7 +98,7 @@ class SettingsMenuDisplay(MenuDisplay):
 | 
			
		||||
 | 
			
		||||
class MainMenuDisplay(Display):
 | 
			
		||||
    """
 | 
			
		||||
    A class to display specifically a mainmenu object
 | 
			
		||||
    A class to display specifically a mainmenu object.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, menu: MainMenu, *args):
 | 
			
		||||
        super().__init__(*args)
 | 
			
		||||
@@ -102,13 +110,18 @@ 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)
 | 
			
		||||
        msg = _("Credits")
 | 
			
		||||
        self.addstr(self.pad, self.height - 1, self.width - 1 - len(msg), msg)
 | 
			
		||||
        self.refresh_pad(self.pad, 0, 0, self.y, self.x,
 | 
			
		||||
                         self.height + self.y - 1,
 | 
			
		||||
                         self.width + self.x - 1)
 | 
			
		||||
@@ -118,26 +131,56 @@ 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:
 | 
			
		||||
    def update(self, game: Game) -> None:
 | 
			
		||||
        self.menudisplay.update_menu(game.main_menu)
 | 
			
		||||
 | 
			
		||||
    def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
 | 
			
		||||
        menuwidth = min(self.menudisplay.preferred_width, self.width)
 | 
			
		||||
        menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
 | 
			
		||||
        menuheight = min(self.menudisplay.preferred_height, self.height - menuy)
 | 
			
		||||
 | 
			
		||||
        if menuy <= y < menuy + menuheight and menux <= x < menux + menuwidth:
 | 
			
		||||
            self.menudisplay.handle_click(y - menuy, x - menux, game)
 | 
			
		||||
            self.menudisplay.handle_click(y - menuy, x - menux, attr, game)
 | 
			
		||||
 | 
			
		||||
        if y <= len(self.title):
 | 
			
		||||
            self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000)
 | 
			
		||||
 | 
			
		||||
        if y == self.height - 1 and x >= self.width - 1 - len(_("Credits")):
 | 
			
		||||
            game.state = GameMode.CREDITS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlayerInventoryDisplay(MenuDisplay):
 | 
			
		||||
    message = _("== INVENTORY ==")
 | 
			
		||||
    """
 | 
			
		||||
    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.addstr(self.pad, 0, (self.width - len(self.message)) // 2,
 | 
			
		||||
                    self.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
 | 
			
		||||
                        + " " + item.translated_name.capitalize())
 | 
			
		||||
            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()
 | 
			
		||||
                        + (f" ({item.description})" if item.description else "")
 | 
			
		||||
                        + (": " + 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:
 | 
			
		||||
@@ -147,27 +190,42 @@ class PlayerInventoryDisplay(MenuDisplay):
 | 
			
		||||
    def trueheight(self) -> int:
 | 
			
		||||
        return 2 + super().trueheight
 | 
			
		||||
 | 
			
		||||
    def handle_click(self, y: int, x: int, game: Game) -> None:
 | 
			
		||||
    def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        We can select a menu item with the mouse.
 | 
			
		||||
        """
 | 
			
		||||
        self.menu.position = max(0, min(len(self.menu.values) - 1, y - 3))
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StoreInventoryDisplay(MenuDisplay):
 | 
			
		||||
    message = _("== STALL ==")
 | 
			
		||||
    """
 | 
			
		||||
    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.addstr(self.pad, 0, (self.width - len(self.message)) // 2,
 | 
			
		||||
                    self.message, curses.A_BOLD | curses.A_ITALIC)
 | 
			
		||||
        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, 2 + i, 0, selection
 | 
			
		||||
            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()
 | 
			
		||||
                        + (f" ({item.description})" if item.description else "")
 | 
			
		||||
                        + ": " + 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)
 | 
			
		||||
@@ -176,9 +234,10 @@ class StoreInventoryDisplay(MenuDisplay):
 | 
			
		||||
    def trueheight(self) -> int:
 | 
			
		||||
        return 2 + super().trueheight
 | 
			
		||||
 | 
			
		||||
    def handle_click(self, y: int, x: int, game: Game) -> None:
 | 
			
		||||
    def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        We can select a menu item with the mouse.
 | 
			
		||||
        """
 | 
			
		||||
        self.menu.position = max(0, min(len(self.menu.values) - 1, y - 3))
 | 
			
		||||
        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)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,12 @@
 | 
			
		||||
import curses
 | 
			
		||||
 | 
			
		||||
from squirrelbattle.display.display import Box, Display
 | 
			
		||||
from squirrelbattle.game import Game
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MessageDisplay(Display):
 | 
			
		||||
    """
 | 
			
		||||
    Display a message in a popup.
 | 
			
		||||
    A class to handle the display of popup messages.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
@@ -17,15 +18,15 @@ 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,
 | 
			
		||||
                         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)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,32 +3,42 @@
 | 
			
		||||
 | 
			
		||||
import curses
 | 
			
		||||
 | 
			
		||||
from ..entities.items import Monocle
 | 
			
		||||
from ..entities.player import Player
 | 
			
		||||
from ..game import Game
 | 
			
		||||
from ..interfaces import FightingEntity
 | 
			
		||||
from ..translations import gettext as _
 | 
			
		||||
from .display import Display
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatsDisplay(Display):
 | 
			
		||||
    """
 | 
			
		||||
    A class to handle the display of the stats of the player.
 | 
			
		||||
    """
 | 
			
		||||
    game: Game
 | 
			
		||||
    player: Player
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    def update(self, game: Game) -> None:
 | 
			
		||||
        self.game = game
 | 
			
		||||
        self.player = game.player
 | 
			
		||||
 | 
			
		||||
    def update_pad(self) -> None:
 | 
			
		||||
        string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\
 | 
			
		||||
            .format(self.player.level, self.player.current_xp,
 | 
			
		||||
                    self.player.max_xp, self.player.health,
 | 
			
		||||
                    self.player.maxhealth)
 | 
			
		||||
        string2 = f"{_(self.player.name).capitalize()} " \
 | 
			
		||||
                  f"-- LVL {self.player.level} -- " \
 | 
			
		||||
                  f"FLOOR {-self.player.map.floor}\n" \
 | 
			
		||||
                  f"EXP {self.player.current_xp}/{self.player.max_xp}\n" \
 | 
			
		||||
                  f"HP {self.player.health}/{self.player.maxhealth}"
 | 
			
		||||
        self.addstr(self.pad, 0, 0, string2)
 | 
			
		||||
        string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\
 | 
			
		||||
            .format(self.player.strength,
 | 
			
		||||
                    self.player.intelligence, self.player.charisma,
 | 
			
		||||
                    self.player.dexterity, self.player.constitution)
 | 
			
		||||
        string3 = f"STR {self.player.strength}\n" \
 | 
			
		||||
                  f"INT {self.player.intelligence}\n" \
 | 
			
		||||
                  f"CHR {self.player.charisma}\n" \
 | 
			
		||||
                  f"DEX {self.player.dexterity}\n" \
 | 
			
		||||
                  f"CON {self.player.constitution}\n" \
 | 
			
		||||
                  f"CRI {self.player.critical}%"
 | 
			
		||||
        self.addstr(self.pad, 3, 0, string3)
 | 
			
		||||
 | 
			
		||||
        inventory_str = _("Inventory:") + " "
 | 
			
		||||
@@ -44,15 +54,73 @@ class StatsDisplay(Display):
 | 
			
		||||
            if count > 1:
 | 
			
		||||
                inventory_str += f"x{count} "
 | 
			
		||||
            printed_items.append(item)
 | 
			
		||||
        self.addstr(self.pad, 8, 0, inventory_str)
 | 
			
		||||
        self.addstr(self.pad, 9, 0, inventory_str)
 | 
			
		||||
 | 
			
		||||
        self.addstr(self.pad, 9, 0, f"{self.pack.HAZELNUT} "
 | 
			
		||||
                                    f"x{self.player.hazel}")
 | 
			
		||||
        if self.player.equipped_main:
 | 
			
		||||
            self.addstr(self.pad, 10, 0,
 | 
			
		||||
                        _("Equipped main:") + " "
 | 
			
		||||
                        f"{self.pack[self.player.equipped_main.name.upper()]}")
 | 
			
		||||
        if self.player.equipped_secondary:
 | 
			
		||||
            self.addstr(self.pad, 11, 0,
 | 
			
		||||
                        _("Equipped secondary:") + " "
 | 
			
		||||
                        + self.pack[self.player.equipped_secondary
 | 
			
		||||
                                    .name.upper()])
 | 
			
		||||
        if self.player.equipped_armor:
 | 
			
		||||
            self.addstr(self.pad, 12, 0,
 | 
			
		||||
                        _("Equipped chestplate:") + " "
 | 
			
		||||
                        + self.pack[self.player.equipped_armor.name.upper()])
 | 
			
		||||
        if self.player.equipped_helmet:
 | 
			
		||||
            self.addstr(self.pad, 13, 0,
 | 
			
		||||
                        _("Equipped helmet:") + " "
 | 
			
		||||
                        + self.pack[self.player.equipped_helmet.name.upper()])
 | 
			
		||||
 | 
			
		||||
        self.addstr(self.pad, 14, 0, f"{self.pack.HAZELNUT} "
 | 
			
		||||
                                     f"x{self.player.hazel}")
 | 
			
		||||
 | 
			
		||||
        if self.player.dead:
 | 
			
		||||
            self.addstr(self.pad, 11, 0, _("YOU ARE DEAD"),
 | 
			
		||||
                        curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
 | 
			
		||||
                        | self.color_pair(3))
 | 
			
		||||
            self.addstr(self.pad, 15, 0, _("YOU ARE DEAD"), curses.COLOR_RED,
 | 
			
		||||
                        bold=True, blink=True, standout=True)
 | 
			
		||||
 | 
			
		||||
        if self.player.map.tiles[self.player.y][self.player.x].is_ladder():
 | 
			
		||||
            msg = _("Use {key} to use the ladder") \
 | 
			
		||||
                .format(key=self.game.settings.KEY_LADDER.upper())
 | 
			
		||||
            self.addstr(self.pad, self.height - 2, 0, msg,
 | 
			
		||||
                        italic=True, reverse=True)
 | 
			
		||||
 | 
			
		||||
        self.update_entities_stats()
 | 
			
		||||
 | 
			
		||||
    def update_entities_stats(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Display information about a near entity if we have a monocle.
 | 
			
		||||
        """
 | 
			
		||||
        for dy, dx in [(-1, 0), (0, -1), (0, 1), (1, 0)]:
 | 
			
		||||
            for entity in self.player.map.find_entities(FightingEntity):
 | 
			
		||||
                if entity == self.player:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                if entity.y == self.player.y + dy \
 | 
			
		||||
                        and entity.x == self.player.x + dx:
 | 
			
		||||
                    if entity.is_friendly():
 | 
			
		||||
                        msg = _("Move to the friendly entity to talk to it") \
 | 
			
		||||
                            if self.game.waiting_for_friendly_key else \
 | 
			
		||||
                            _("Use {key} then move to talk to the entity") \
 | 
			
		||||
                            .format(key=self.game.settings.KEY_CHAT.upper())
 | 
			
		||||
                        self.addstr(self.pad, self.height - 1, 0, msg,
 | 
			
		||||
                                    italic=True, reverse=True)
 | 
			
		||||
 | 
			
		||||
                    if isinstance(self.player.equipped_secondary, Monocle):
 | 
			
		||||
                        # Truth monocle
 | 
			
		||||
                        message = f"{entity.translated_name.capitalize()} " \
 | 
			
		||||
                                  f"{self.pack[entity.name.upper()]}\n" \
 | 
			
		||||
                                  f"STR {entity.strength}\n" \
 | 
			
		||||
                                  f"INT {entity.intelligence}\n" \
 | 
			
		||||
                                  f"CHR {entity.charisma}\n" \
 | 
			
		||||
                                  f"DEX {entity.dexterity}\n" \
 | 
			
		||||
                                  f"CON {entity.constitution}\n" \
 | 
			
		||||
                                  f"CRI {entity.critical}%"
 | 
			
		||||
                        self.addstr(self.pad, 17, 0, message)
 | 
			
		||||
                    # Only display one entity
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
    def display(self) -> None:
 | 
			
		||||
        self.pad.erase()
 | 
			
		||||
 
 | 
			
		||||
@@ -2,33 +2,44 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
import curses
 | 
			
		||||
from typing import Any
 | 
			
		||||
from typing import Any, Union, Tuple
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TexturePack:
 | 
			
		||||
    """
 | 
			
		||||
    A class to handle displaying several textures.
 | 
			
		||||
    """
 | 
			
		||||
    _packs = dict()
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    tile_width: int
 | 
			
		||||
    tile_fg_color: int
 | 
			
		||||
    tile_bg_color: int
 | 
			
		||||
    entity_fg_color: int
 | 
			
		||||
    entity_bg_color: int
 | 
			
		||||
    tile_fg_color: Union[int, Tuple[int, int, int]]
 | 
			
		||||
    tile_fg_visible_color: Union[int, Tuple[int, int, int]]
 | 
			
		||||
    tile_bg_color: Union[int, Tuple[int, int, int]]
 | 
			
		||||
    entity_fg_color: Union[int, Tuple[int, int, int]]
 | 
			
		||||
    entity_bg_color: Union[int, Tuple[int, int, int]]
 | 
			
		||||
 | 
			
		||||
    BODY_SNATCH_POTION: str
 | 
			
		||||
    BOMB: str
 | 
			
		||||
    HEART: str
 | 
			
		||||
    HEDGEHOG: str
 | 
			
		||||
    CHESTPLATE: str
 | 
			
		||||
    EAGLE: str
 | 
			
		||||
    EMPTY: str
 | 
			
		||||
    FLOOR: str
 | 
			
		||||
    HAZELNUT: str
 | 
			
		||||
    HEART: str
 | 
			
		||||
    HEDGEHOG: str
 | 
			
		||||
    HELMET: str
 | 
			
		||||
    MERCHANT: str
 | 
			
		||||
    PLAYER: str
 | 
			
		||||
    RABBIT: str
 | 
			
		||||
    RING_OF_CRITICAL_DAMAGE: str
 | 
			
		||||
    RING_OF_MORE_EXPERIENCE: str
 | 
			
		||||
    SHIELD: str
 | 
			
		||||
    SUNFLOWER: str
 | 
			
		||||
    SWORD: str
 | 
			
		||||
    TEDDY_BEAR: str
 | 
			
		||||
    TIGER: str
 | 
			
		||||
    TRUMPET: str
 | 
			
		||||
    WALL: str
 | 
			
		||||
 | 
			
		||||
    ASCII_PACK: "TexturePack"
 | 
			
		||||
@@ -54,49 +65,72 @@ class TexturePack:
 | 
			
		||||
TexturePack.ASCII_PACK = TexturePack(
 | 
			
		||||
    name="ascii",
 | 
			
		||||
    tile_width=1,
 | 
			
		||||
    tile_fg_visible_color=(1000, 1000, 1000),
 | 
			
		||||
    tile_fg_color=curses.COLOR_WHITE,
 | 
			
		||||
    tile_bg_color=curses.COLOR_BLACK,
 | 
			
		||||
    entity_fg_color=curses.COLOR_WHITE,
 | 
			
		||||
    entity_fg_color=(1000, 1000, 1000),
 | 
			
		||||
    entity_bg_color=curses.COLOR_BLACK,
 | 
			
		||||
 | 
			
		||||
    BODY_SNATCH_POTION='S',
 | 
			
		||||
    BOMB='o',
 | 
			
		||||
    BOMB='ç',
 | 
			
		||||
    CHESTPLATE='(',
 | 
			
		||||
    EAGLE='µ',
 | 
			
		||||
    EMPTY=' ',
 | 
			
		||||
    EXPLOSION='%',
 | 
			
		||||
    FLOOR='.',
 | 
			
		||||
    LADDER='H',
 | 
			
		||||
    HAZELNUT='¤',
 | 
			
		||||
    HEART='❤',
 | 
			
		||||
    HEDGEHOG='*',
 | 
			
		||||
    HELMET='0',
 | 
			
		||||
    MERCHANT='M',
 | 
			
		||||
    MONOCLE='ô',
 | 
			
		||||
    PLAYER='@',
 | 
			
		||||
    RABBIT='Y',
 | 
			
		||||
    RING_OF_CRITICAL_DAMAGE='o',
 | 
			
		||||
    RING_OF_MORE_EXPERIENCE='o',
 | 
			
		||||
    SHIELD='D',
 | 
			
		||||
    SUNFLOWER='I',
 | 
			
		||||
    SWORD='\u2020',
 | 
			
		||||
    TEDDY_BEAR='8',
 | 
			
		||||
    TIGER='n',
 | 
			
		||||
    TRUMPET='/',
 | 
			
		||||
    WALL='#',
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
TexturePack.SQUIRREL_PACK = TexturePack(
 | 
			
		||||
    name="squirrel",
 | 
			
		||||
    tile_width=2,
 | 
			
		||||
    tile_fg_visible_color=(1000, 1000, 1000),
 | 
			
		||||
    tile_fg_color=curses.COLOR_WHITE,
 | 
			
		||||
    tile_bg_color=curses.COLOR_BLACK,
 | 
			
		||||
    entity_fg_color=curses.COLOR_WHITE,
 | 
			
		||||
    entity_bg_color=curses.COLOR_WHITE,
 | 
			
		||||
    entity_fg_color=(1000, 1000, 1000),
 | 
			
		||||
    entity_bg_color=(1000, 1000, 1000),
 | 
			
		||||
 | 
			
		||||
    BODY_SNATCH_POTION='🔀',
 | 
			
		||||
    BOMB='💣',
 | 
			
		||||
    CHESTPLATE='🦺',
 | 
			
		||||
    EAGLE='🦅',
 | 
			
		||||
    EMPTY='  ',
 | 
			
		||||
    EXPLOSION='💥',
 | 
			
		||||
    FLOOR='██',
 | 
			
		||||
    LADDER=('🪜', curses.COLOR_WHITE, (1000, 1000, 1000),
 | 
			
		||||
            curses.COLOR_WHITE, (1000, 1000, 1000)),
 | 
			
		||||
    HAZELNUT='🌰',
 | 
			
		||||
    HEART='💜',
 | 
			
		||||
    HEDGEHOG='🦔',
 | 
			
		||||
    HELMET='⛑️ ',
 | 
			
		||||
    PLAYER='🐿️ ️',
 | 
			
		||||
    MERCHANT='🦜',
 | 
			
		||||
    MONOCLE='🧐',
 | 
			
		||||
    RABBIT='🐇',
 | 
			
		||||
    RING_OF_CRITICAL_DAMAGE='💍',
 | 
			
		||||
    RING_OF_MORE_EXPERIENCE='💍',
 | 
			
		||||
    SHIELD='🛡️ ',
 | 
			
		||||
    SUNFLOWER='🌻',
 | 
			
		||||
    SWORD='🗡️',
 | 
			
		||||
    SWORD='🗡️ ',
 | 
			
		||||
    TEDDY_BEAR='🧸',
 | 
			
		||||
    TIGER='🐅',
 | 
			
		||||
    TRUMPET='🎺',
 | 
			
		||||
    WALL='🧱',
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,18 @@
 | 
			
		||||
from ..interfaces import FriendlyEntity, InventoryHolder
 | 
			
		||||
from ..interfaces import FriendlyEntity, InventoryHolder, Map, FightingEntity
 | 
			
		||||
from ..translations import gettext as _
 | 
			
		||||
from .player import Player
 | 
			
		||||
from .monsters import Monster
 | 
			
		||||
from .items import Item
 | 
			
		||||
from random import choice
 | 
			
		||||
from random import choice, shuffle
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Merchant(InventoryHolder, FriendlyEntity):
 | 
			
		||||
    """
 | 
			
		||||
    The class for merchants in the dungeon
 | 
			
		||||
    The class of merchants in the dungeon.
 | 
			
		||||
    """
 | 
			
		||||
    def keys(self) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Returns a friendly entitie's specific attributes
 | 
			
		||||
        Returns a friendly entitie's specific attributes.
 | 
			
		||||
        """
 | 
			
		||||
        return super().keys() + ["inventory", "hazel"]
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +21,6 @@ class Merchant(InventoryHolder, FriendlyEntity):
 | 
			
		||||
        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())())
 | 
			
		||||
@@ -28,23 +28,101 @@ class Merchant(InventoryHolder, FriendlyEntity):
 | 
			
		||||
    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
 | 
			
		||||
        and allows 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.
 | 
			
		||||
        Changes the number of hazel the merchant has by hz.
 | 
			
		||||
        """
 | 
			
		||||
        self.hazel += hz
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sunflower(FriendlyEntity):
 | 
			
		||||
    """
 | 
			
		||||
    A friendly sunflower
 | 
			
		||||
    A friendly sunflower.
 | 
			
		||||
    """
 | 
			
		||||
    dialogue_option = [_("Flower power!!"), _("The sun is warm today")]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, maxhealth: int = 15,
 | 
			
		||||
                 *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(name="sunflower", maxhealth=maxhealth, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def dialogue_option(self) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Lists all that a sunflower can say to the player.
 | 
			
		||||
        """
 | 
			
		||||
        return [_("Flower power!!"), _("The sun is warm today")]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Familiar(FightingEntity):
 | 
			
		||||
    """
 | 
			
		||||
    A friendly familiar that helps the player defeat monsters.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, maxhealth: int = 25,
 | 
			
		||||
                 *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(maxhealth=maxhealth, *args, **kwargs)
 | 
			
		||||
        self.target = None
 | 
			
		||||
 | 
			
		||||
#    @property
 | 
			
		||||
#    def dialogue_option(self) -> list:
 | 
			
		||||
#        """
 | 
			
		||||
#        Debug function (to see if used in the real game)
 | 
			
		||||
#        """
 | 
			
		||||
#        return [_("My target is"+str(self.target))]
 | 
			
		||||
 | 
			
		||||
    def act(self, p: Player, m: Map) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        By default, the familiar tries to stay at distance at most 2 of the
 | 
			
		||||
        player and if a monster comes in range 3, it focuses on the monster
 | 
			
		||||
        and attacks it.
 | 
			
		||||
        """
 | 
			
		||||
        if self.target is None:
 | 
			
		||||
            # If the previous target is dead(or if there was no previous target)
 | 
			
		||||
            # the familiar tries to get closer to the player.
 | 
			
		||||
            self.target = p
 | 
			
		||||
        elif self.target.dead:
 | 
			
		||||
            self.target = p
 | 
			
		||||
        if self.target == p:
 | 
			
		||||
            # Look for monsters around the player to kill TOFIX : if monster is
 | 
			
		||||
            # out of range, continue targetting player.
 | 
			
		||||
            for entity in m.entities:
 | 
			
		||||
                if (p.y - entity.y) ** 2 + (p.x - entity.x) ** 2 <= 9 and\
 | 
			
		||||
                        isinstance(entity, Monster):
 | 
			
		||||
                    self.target = entity
 | 
			
		||||
                    entity.paths = dict()  # Allows the paths to be calculated.
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
        # Familiars move according to a Dijkstra algorithm
 | 
			
		||||
        # that targets their target.
 | 
			
		||||
        # If they can not move and are already close to their target,
 | 
			
		||||
        # they hit, except if their target is the player.
 | 
			
		||||
        if self.target and (self.y, self.x) in self.target.paths:
 | 
			
		||||
            # Moves to target player by choosing the best available path
 | 
			
		||||
            for next_y, next_x in self.target.paths[(self.y, self.x)]:
 | 
			
		||||
                moved = self.check_move(next_y, next_x, True)
 | 
			
		||||
                if moved:
 | 
			
		||||
                    break
 | 
			
		||||
                if self.distance_squared(self.target) <= 1 and \
 | 
			
		||||
                        not isinstance(self.target, Player):
 | 
			
		||||
                    self.map.logs.add_message(self.hit(self.target))
 | 
			
		||||
                    break
 | 
			
		||||
        else:
 | 
			
		||||
            # Moves in a random direction
 | 
			
		||||
            # If the direction is not available, tries another one
 | 
			
		||||
            moves = [self.move_up, self.move_down,
 | 
			
		||||
                     self.move_left, self.move_right]
 | 
			
		||||
            shuffle(moves)
 | 
			
		||||
            for move in moves:
 | 
			
		||||
                if move():
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Trumpet(Familiar):
 | 
			
		||||
    """
 | 
			
		||||
    A class of familiars.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "trumpet", strength: int = 3,
 | 
			
		||||
                 maxhealth: int = 20, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(name=name, strength=strength,
 | 
			
		||||
                         maxhealth=maxhealth, *args, **kwargs)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,13 @@
 | 
			
		||||
from random import choice, randint
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from .player import Player
 | 
			
		||||
from ..interfaces import Entity, FightingEntity, Map, InventoryHolder
 | 
			
		||||
from ..translations import gettext as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Item(Entity):
 | 
			
		||||
    """
 | 
			
		||||
    A class for items
 | 
			
		||||
    A class for items.
 | 
			
		||||
    """
 | 
			
		||||
    held: bool
 | 
			
		||||
    held_by: Optional[InventoryHolder]
 | 
			
		||||
@@ -25,12 +24,19 @@ class Item(Entity):
 | 
			
		||||
        self.held_by = held_by
 | 
			
		||||
        self.price = price
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        In the inventory, indicate the usefulness of the item.
 | 
			
		||||
        """
 | 
			
		||||
        return ""
 | 
			
		||||
 | 
			
		||||
    def drop(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        The item is dropped from the inventory onto the floor
 | 
			
		||||
        The item is dropped from the inventory onto the floor.
 | 
			
		||||
        """
 | 
			
		||||
        if self.held:
 | 
			
		||||
            self.held_by.inventory.remove(self)
 | 
			
		||||
            self.held_by.remove_from_inventory(self)
 | 
			
		||||
            self.held_by.map.add_entity(self)
 | 
			
		||||
            self.move(self.held_by.y, self.held_by.x)
 | 
			
		||||
            self.held = False
 | 
			
		||||
@@ -45,19 +51,31 @@ class Item(Entity):
 | 
			
		||||
        """
 | 
			
		||||
        Indicates what should be done when the item is equipped.
 | 
			
		||||
        """
 | 
			
		||||
        # Other objects are only equipped as secondary.
 | 
			
		||||
        if self.held_by.equipped_secondary:
 | 
			
		||||
            self.held_by.equipped_secondary.unequip()
 | 
			
		||||
        self.held_by.remove_from_inventory(self)
 | 
			
		||||
        self.held_by.equipped_secondary = self
 | 
			
		||||
 | 
			
		||||
    def hold(self, player: InventoryHolder) -> None:
 | 
			
		||||
    def unequip(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        The item is taken from the floor and put into the inventory
 | 
			
		||||
        Indicates what should be done when the item is unequipped.
 | 
			
		||||
        """
 | 
			
		||||
        self.held_by.remove_from_inventory(self)
 | 
			
		||||
        self.held_by.add_to_inventory(self)
 | 
			
		||||
 | 
			
		||||
    def hold(self, holder: InventoryHolder) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        The item is taken from the floor and put into the inventory.
 | 
			
		||||
        """
 | 
			
		||||
        self.held = True
 | 
			
		||||
        self.held_by = player
 | 
			
		||||
        self.held_by = holder
 | 
			
		||||
        self.held_by.map.remove_entity(self)
 | 
			
		||||
        player.add_to_inventory(self)
 | 
			
		||||
        holder.add_to_inventory(self)
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the state of the entity into a dictionary
 | 
			
		||||
        Saves the state of the item into a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        d["held"] = self.held
 | 
			
		||||
@@ -65,13 +83,17 @@ class Item(Entity):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_items() -> list:
 | 
			
		||||
        return [BodySnatchPotion, Bomb, Heart, Sword]
 | 
			
		||||
        """
 | 
			
		||||
        Returns the list of all item classes.
 | 
			
		||||
        """
 | 
			
		||||
        return [BodySnatchPotion, Chestplate, Bomb, Heart, Helmet, Monocle,
 | 
			
		||||
                Shield, Sword, RingCritical, RingXP]
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        inventory.
 | 
			
		||||
        """
 | 
			
		||||
        if buyer.hazel >= self.price:
 | 
			
		||||
            self.hold(buyer)
 | 
			
		||||
@@ -85,7 +107,7 @@ class Item(Entity):
 | 
			
		||||
 | 
			
		||||
class Heart(Item):
 | 
			
		||||
    """
 | 
			
		||||
    A heart item to return health to the player
 | 
			
		||||
    A heart item to return health to the player.
 | 
			
		||||
    """
 | 
			
		||||
    healing: int
 | 
			
		||||
 | 
			
		||||
@@ -94,16 +116,21 @@ class Heart(Item):
 | 
			
		||||
        super().__init__(name=name, price=price, *args, **kwargs)
 | 
			
		||||
        self.healing = healing
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self) -> str:
 | 
			
		||||
        return f"HP+{self.healing}"
 | 
			
		||||
 | 
			
		||||
    def hold(self, entity: InventoryHolder) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        When holding a heart, heal the player and don't put item in inventory.
 | 
			
		||||
        When holding a heart, the player is healed and
 | 
			
		||||
        the item is not put in the inventory.
 | 
			
		||||
        """
 | 
			
		||||
        entity.health = min(entity.maxhealth, entity.health + self.healing)
 | 
			
		||||
        entity.map.remove_entity(self)
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the state of the header into a dictionary
 | 
			
		||||
        Saves the state of the heart into a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        d["healing"] = self.healing
 | 
			
		||||
@@ -116,7 +143,7 @@ class Bomb(Item):
 | 
			
		||||
    """
 | 
			
		||||
    damage: int = 5
 | 
			
		||||
    exploding: bool
 | 
			
		||||
    owner: Optional["Player"]
 | 
			
		||||
    owner: Optional["InventoryHolder"]
 | 
			
		||||
    tick: int
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name: str = "bomb", damage: int = 5,
 | 
			
		||||
@@ -129,7 +156,7 @@ class Bomb(Item):
 | 
			
		||||
 | 
			
		||||
    def use(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        When the bomb is used, throw it and explodes it.
 | 
			
		||||
        When the bomb is used, it is thrown and then it explodes.
 | 
			
		||||
        """
 | 
			
		||||
        if self.held:
 | 
			
		||||
            self.owner = self.held_by
 | 
			
		||||
@@ -138,7 +165,7 @@ class Bomb(Item):
 | 
			
		||||
 | 
			
		||||
    def act(self, m: Map) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Special exploding action of the bomb
 | 
			
		||||
        Special exploding action of the bomb.
 | 
			
		||||
        """
 | 
			
		||||
        if self.exploding:
 | 
			
		||||
            if self.tick > 0:
 | 
			
		||||
@@ -158,9 +185,13 @@ 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
 | 
			
		||||
        Saves the state of the bomb into a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        d["exploding"] = self.exploding
 | 
			
		||||
@@ -168,6 +199,25 @@ 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 bomb disappears after exploding.
 | 
			
		||||
        """
 | 
			
		||||
        m.remove_entity(self)
 | 
			
		||||
 | 
			
		||||
    def hold(self, player: InventoryHolder) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        The player can't hold an explosion.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Weapon(Item):
 | 
			
		||||
    """
 | 
			
		||||
    Non-throwable items that improve player damage
 | 
			
		||||
@@ -178,6 +228,10 @@ class Weapon(Item):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.damage = damage
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self) -> str:
 | 
			
		||||
        return f"STR+{self.damage}" if self.damage else super().description
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the state of the weapon into a dictionary
 | 
			
		||||
@@ -186,14 +240,101 @@ class Weapon(Item):
 | 
			
		||||
        d["damage"] = self.damage
 | 
			
		||||
        return d
 | 
			
		||||
 | 
			
		||||
    def equip(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        When a weapon is equipped, the player gains strength.
 | 
			
		||||
        """
 | 
			
		||||
        self.held_by.remove_from_inventory(self)
 | 
			
		||||
        self.held_by.equipped_main = self
 | 
			
		||||
        self.held_by.strength += self.damage
 | 
			
		||||
 | 
			
		||||
    def unequip(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Remove the strength earned by the weapon.
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        super().unequip()
 | 
			
		||||
        self.held_by.strength -= self.damage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sword(Weapon):
 | 
			
		||||
    """
 | 
			
		||||
    A basic weapon
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "sword", price: int = 20, *args, **kwargs):
 | 
			
		||||
    def __init__(self, name: str = "sword", price: int = 20,
 | 
			
		||||
                 *args, **kwargs):
 | 
			
		||||
        super().__init__(name=name, price=price, *args, **kwargs)
 | 
			
		||||
        self.name = name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Armor(Item):
 | 
			
		||||
    """
 | 
			
		||||
    Class of items that increase the player's constitution.
 | 
			
		||||
    """
 | 
			
		||||
    constitution: int
 | 
			
		||||
 | 
			
		||||
    def __init__(self, constitution: int, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.constitution = constitution
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self) -> str:
 | 
			
		||||
        return f"CON+{self.constitution}" if self.constitution \
 | 
			
		||||
            else super().description
 | 
			
		||||
 | 
			
		||||
    def equip(self) -> None:
 | 
			
		||||
        super().equip()
 | 
			
		||||
        self.held_by.constitution += self.constitution
 | 
			
		||||
 | 
			
		||||
    def unequip(self) -> None:
 | 
			
		||||
        super().unequip()
 | 
			
		||||
        self.held_by.constitution -= self.constitution
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        d["constitution"] = self.constitution
 | 
			
		||||
        return d
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Shield(Armor):
 | 
			
		||||
    """
 | 
			
		||||
    Class of shield items, they can be equipped in the other hand.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "shield", constitution: int = 2,
 | 
			
		||||
                 price: int = 16, *args, **kwargs):
 | 
			
		||||
        super().__init__(name=name, constitution=constitution, price=price,
 | 
			
		||||
                         *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Helmet(Armor):
 | 
			
		||||
    """
 | 
			
		||||
    Class of helmet items, they can be equipped on the head.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "helmet", constitution: int = 2,
 | 
			
		||||
                 price: int = 18, *args, **kwargs):
 | 
			
		||||
        super().__init__(name=name, constitution=constitution, price=price,
 | 
			
		||||
                         *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def equip(self) -> None:
 | 
			
		||||
        if self.held_by.equipped_helmet:
 | 
			
		||||
            self.held_by.equipped_helmet.unequip()
 | 
			
		||||
        self.held_by.remove_from_inventory(self)
 | 
			
		||||
        self.held_by.equipped_helmet = self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Chestplate(Armor):
 | 
			
		||||
    """
 | 
			
		||||
    Class of chestplate items, they can be equipped on the body.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "chestplate", constitution: int = 4,
 | 
			
		||||
                 price: int = 30, *args, **kwargs):
 | 
			
		||||
        super().__init__(name=name, constitution=constitution, price=price,
 | 
			
		||||
                         *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def equip(self) -> None:
 | 
			
		||||
        if self.held_by.equipped_armor:
 | 
			
		||||
            self.held_by.equipped_armor.unequip()
 | 
			
		||||
        self.held_by.remove_from_inventory(self)
 | 
			
		||||
        self.held_by.equipped_armor = self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BodySnatchPotion(Item):
 | 
			
		||||
@@ -228,3 +369,93 @@ class BodySnatchPotion(Item):
 | 
			
		||||
        self.held_by.recalculate_paths()
 | 
			
		||||
 | 
			
		||||
        self.held_by.inventory.remove(self)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Ring(Item):
 | 
			
		||||
    """
 | 
			
		||||
    A class of rings that boost the player's statistics.
 | 
			
		||||
    """
 | 
			
		||||
    maxhealth: int
 | 
			
		||||
    strength: int
 | 
			
		||||
    intelligence: int
 | 
			
		||||
    charisma: int
 | 
			
		||||
    dexterity: int
 | 
			
		||||
    constitution: int
 | 
			
		||||
    critical: int
 | 
			
		||||
    experience: float
 | 
			
		||||
 | 
			
		||||
    def __init__(self, maxhealth: int = 0, strength: int = 0,
 | 
			
		||||
                 intelligence: int = 0, charisma: int = 0,
 | 
			
		||||
                 dexterity: int = 0, constitution: int = 0,
 | 
			
		||||
                 critical: int = 0, experience: float = 0, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.maxhealth = maxhealth
 | 
			
		||||
        self.strength = strength
 | 
			
		||||
        self.intelligence = intelligence
 | 
			
		||||
        self.charisma = charisma
 | 
			
		||||
        self.dexterity = dexterity
 | 
			
		||||
        self.constitution = constitution
 | 
			
		||||
        self.critical = critical
 | 
			
		||||
        self.experience = experience
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self) -> str:
 | 
			
		||||
        fields = [("MAX HP", self.maxhealth), ("STR", self.strength),
 | 
			
		||||
                  ("INT", self.intelligence), ("CHR", self.charisma),
 | 
			
		||||
                  ("DEX", self.dexterity), ("CON", self.constitution),
 | 
			
		||||
                  ("CRI", self.critical), ("XP", self.experience)]
 | 
			
		||||
        return ", ".join(f"{key}+{value}" for key, value in fields if value)
 | 
			
		||||
 | 
			
		||||
    def equip(self) -> None:
 | 
			
		||||
        super().equip()
 | 
			
		||||
        self.held_by.maxhealth += self.maxhealth
 | 
			
		||||
        self.held_by.strength += self.strength
 | 
			
		||||
        self.held_by.intelligence += self.intelligence
 | 
			
		||||
        self.held_by.charisma += self.charisma
 | 
			
		||||
        self.held_by.dexterity += self.dexterity
 | 
			
		||||
        self.held_by.constitution += self.constitution
 | 
			
		||||
        self.held_by.critical += self.critical
 | 
			
		||||
        self.held_by.xp_buff += self.experience
 | 
			
		||||
 | 
			
		||||
    def unequip(self) -> None:
 | 
			
		||||
        super().unequip()
 | 
			
		||||
        self.held_by.maxhealth -= self.maxhealth
 | 
			
		||||
        self.held_by.strength -= self.strength
 | 
			
		||||
        self.held_by.intelligence -= self.intelligence
 | 
			
		||||
        self.held_by.charisma -= self.charisma
 | 
			
		||||
        self.held_by.dexterity -= self.dexterity
 | 
			
		||||
        self.held_by.constitution -= self.constitution
 | 
			
		||||
        self.held_by.critical -= self.critical
 | 
			
		||||
        self.held_by.xp_buff -= self.experience
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        d["maxhealth"] = self.maxhealth
 | 
			
		||||
        d["strength"] = self.strength
 | 
			
		||||
        d["intelligence"] = self.intelligence
 | 
			
		||||
        d["charisma"] = self.charisma
 | 
			
		||||
        d["dexterity"] = self.dexterity
 | 
			
		||||
        d["constitution"] = self.constitution
 | 
			
		||||
        d["critical"] = self.critical
 | 
			
		||||
        d["experience"] = self.experience
 | 
			
		||||
        return d
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RingCritical(Ring):
 | 
			
		||||
    def __init__(self, name: str = "ring_of_critical_damage", price: int = 15,
 | 
			
		||||
                 critical: int = 20, *args, **kwargs):
 | 
			
		||||
        super().__init__(name=name, price=price, critical=critical,
 | 
			
		||||
                         *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RingXP(Ring):
 | 
			
		||||
    def __init__(self, name: str = "ring_of_more_experience", price: int = 25,
 | 
			
		||||
                 experience: float = 2, *args, **kwargs):
 | 
			
		||||
        super().__init__(name=name, price=price, experience=experience,
 | 
			
		||||
                         *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Monocle(Item):
 | 
			
		||||
    def __init__(self, name: str = "monocle", price: int = 10,
 | 
			
		||||
                 *args, **kwargs):
 | 
			
		||||
        super().__init__(name=name, price=price, *args, **kwargs)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@ from ..interfaces import FightingEntity, Map
 | 
			
		||||
class Monster(FightingEntity):
 | 
			
		||||
    """
 | 
			
		||||
    The class for all monsters in the dungeon.
 | 
			
		||||
    A monster must override this class, and the parameters are given
 | 
			
		||||
    in the __init__ function.
 | 
			
		||||
    All specific monster classes overwrite this class,
 | 
			
		||||
    and the parameters are given in the __init__ function.
 | 
			
		||||
    An example of the specification of a monster that has a strength of 4
 | 
			
		||||
    and 20 max HP:
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +21,7 @@ class Monster(FightingEntity):
 | 
			
		||||
            super().__init__(name="my_monster", strength=strength,
 | 
			
		||||
                             maxhealth=maxhealth, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    With that way, attributes can be overwritten when the entity got created.
 | 
			
		||||
    With that way, attributes can be overwritten when the entity is created.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
@@ -29,7 +29,7 @@ class Monster(FightingEntity):
 | 
			
		||||
    def act(self, m: Map) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        By default, a monster will move randomly where it is possible
 | 
			
		||||
        And if a player is close to the monster, the monster run on the player.
 | 
			
		||||
        If the player is closeby, the monster runs to the player.
 | 
			
		||||
        """
 | 
			
		||||
        target = None
 | 
			
		||||
        for entity in m.entities:
 | 
			
		||||
@@ -38,12 +38,12 @@ class Monster(FightingEntity):
 | 
			
		||||
                target = entity
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        # A Dijkstra algorithm has ran that targets the player.
 | 
			
		||||
        # With that way, monsters can simply follow the path.
 | 
			
		||||
        # If they can't move and they are already close to the player,
 | 
			
		||||
        # They hit.
 | 
			
		||||
        # Monsters move according to a Dijkstra algorithm
 | 
			
		||||
        # that targets the player.
 | 
			
		||||
        # If they can not move and are already close to the player,
 | 
			
		||||
        # they hit.
 | 
			
		||||
        if target and (self.y, self.x) in target.paths:
 | 
			
		||||
            # Move to target player by choosing the best avaliable path
 | 
			
		||||
            # Moves to target player by choosing the best available path
 | 
			
		||||
            for next_y, next_x in target.paths[(self.y, self.x)]:
 | 
			
		||||
                moved = self.check_move(next_y, next_x, True)
 | 
			
		||||
                if moved:
 | 
			
		||||
@@ -52,8 +52,8 @@ class Monster(FightingEntity):
 | 
			
		||||
                    self.map.logs.add_message(self.hit(target))
 | 
			
		||||
                    break
 | 
			
		||||
        else:
 | 
			
		||||
            # Move in a random direction
 | 
			
		||||
            # If the direction is not available, try another one
 | 
			
		||||
            # Moves in a random direction
 | 
			
		||||
            # If the direction is not available, tries another one
 | 
			
		||||
            moves = [self.move_up, self.move_down,
 | 
			
		||||
                     self.move_left, self.move_right]
 | 
			
		||||
            shuffle(moves)
 | 
			
		||||
@@ -61,10 +61,17 @@ class Monster(FightingEntity):
 | 
			
		||||
                if move():
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
    def move(self, y: int, x: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Overwrites the move function to recalculate paths.
 | 
			
		||||
        """
 | 
			
		||||
        super().move(y, x)
 | 
			
		||||
        self.recalculate_paths()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Tiger(Monster):
 | 
			
		||||
    """
 | 
			
		||||
    A tiger monster
 | 
			
		||||
    A tiger monster.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "tiger", strength: int = 2,
 | 
			
		||||
                 maxhealth: int = 20, *args, **kwargs) -> None:
 | 
			
		||||
@@ -74,7 +81,7 @@ class Tiger(Monster):
 | 
			
		||||
 | 
			
		||||
class Hedgehog(Monster):
 | 
			
		||||
    """
 | 
			
		||||
    A really mean hedgehog monster
 | 
			
		||||
    A really mean hedgehog monster.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "hedgehog", strength: int = 3,
 | 
			
		||||
                 maxhealth: int = 10, *args, **kwargs) -> None:
 | 
			
		||||
@@ -84,19 +91,31 @@ class Hedgehog(Monster):
 | 
			
		||||
 | 
			
		||||
class Rabbit(Monster):
 | 
			
		||||
    """
 | 
			
		||||
    A rabbit monster
 | 
			
		||||
    A rabbit monster.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "rabbit", strength: int = 1,
 | 
			
		||||
                 maxhealth: int = 15, *args, **kwargs) -> None:
 | 
			
		||||
                 maxhealth: int = 15, critical: int = 30,
 | 
			
		||||
                 *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(name=name, strength=strength,
 | 
			
		||||
                         maxhealth=maxhealth, *args, **kwargs)
 | 
			
		||||
                         maxhealth=maxhealth, critical=critical,
 | 
			
		||||
                         *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TeddyBear(Monster):
 | 
			
		||||
    """
 | 
			
		||||
    A cute teddybear monster
 | 
			
		||||
    A cute teddybear monster.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "teddy_bear", strength: int = 0,
 | 
			
		||||
                 maxhealth: int = 50, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(name=name, strength=strength,
 | 
			
		||||
                         maxhealth=maxhealth, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GiantSeaEagle(Monster):
 | 
			
		||||
    """
 | 
			
		||||
    An eagle boss
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, name: str = "eagle", strength: int = 1000,
 | 
			
		||||
                 maxhealth: int = 5000, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(name=name, strength=strength,
 | 
			
		||||
                         maxhealth=maxhealth, *args, **kwargs)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +1,54 @@
 | 
			
		||||
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from functools import reduce
 | 
			
		||||
from queue import PriorityQueue
 | 
			
		||||
from random import randint
 | 
			
		||||
from typing import Dict, Tuple
 | 
			
		||||
from typing import Dict, Optional, Tuple
 | 
			
		||||
 | 
			
		||||
from .items import Item
 | 
			
		||||
from ..interfaces import FightingEntity, InventoryHolder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Player(InventoryHolder, FightingEntity):
 | 
			
		||||
    """
 | 
			
		||||
    The class of the player
 | 
			
		||||
    The class of the player.
 | 
			
		||||
    """
 | 
			
		||||
    current_xp: int = 0
 | 
			
		||||
    max_xp: int = 10
 | 
			
		||||
    xp_buff: float = 1
 | 
			
		||||
    paths: Dict[Tuple[int, int], Tuple[int, int]]
 | 
			
		||||
    equipped_main: Optional[Item]
 | 
			
		||||
    equipped_secondary: Optional[Item]
 | 
			
		||||
    equipped_helmet: Optional[Item]
 | 
			
		||||
    equipped_armor: Optional[Item]
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
                 hazel: int = 42, *args, **kwargs) \
 | 
			
		||||
            -> None:
 | 
			
		||||
                 hazel: int = 42, equipped_main: Optional[Item] = None,
 | 
			
		||||
                 equipped_armor: Optional[Item] = None, critical: int = 5,
 | 
			
		||||
                 equipped_secondary: Optional[Item] = None,
 | 
			
		||||
                 equipped_helmet: Optional[Item] = None, xp_buff: float = 1,
 | 
			
		||||
                 vision: int = 5, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(name=name, maxhealth=maxhealth, strength=strength,
 | 
			
		||||
                         intelligence=intelligence, charisma=charisma,
 | 
			
		||||
                         dexterity=dexterity, constitution=constitution,
 | 
			
		||||
                         level=level, *args, **kwargs)
 | 
			
		||||
                         level=level, critical=critical, *args, **kwargs)
 | 
			
		||||
        self.current_xp = current_xp
 | 
			
		||||
        self.max_xp = max_xp
 | 
			
		||||
        self.xp_buff = xp_buff
 | 
			
		||||
        self.inventory = self.translate_inventory(inventory or [])
 | 
			
		||||
        self.paths = dict()
 | 
			
		||||
        self.hazel = hazel
 | 
			
		||||
        self.equipped_main = self.dict_to_item(equipped_main) \
 | 
			
		||||
            if isinstance(equipped_main, dict) else equipped_main
 | 
			
		||||
        self.equipped_armor = self.dict_to_item(equipped_armor) \
 | 
			
		||||
            if isinstance(equipped_armor, dict) else equipped_armor
 | 
			
		||||
        self.equipped_secondary = self.dict_to_item(equipped_secondary) \
 | 
			
		||||
            if isinstance(equipped_secondary, dict) else equipped_secondary
 | 
			
		||||
        self.equipped_helmet = self.dict_to_item(equipped_helmet) \
 | 
			
		||||
            if isinstance(equipped_helmet, dict) else equipped_helmet
 | 
			
		||||
        self.vision = vision
 | 
			
		||||
 | 
			
		||||
    def move(self, y: int, x: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
@@ -42,10 +59,11 @@ class Player(InventoryHolder, FightingEntity):
 | 
			
		||||
        self.map.currenty = y
 | 
			
		||||
        self.map.currentx = x
 | 
			
		||||
        self.recalculate_paths()
 | 
			
		||||
        self.map.compute_visibility(self.y, self.x, self.vision)
 | 
			
		||||
 | 
			
		||||
    def level_up(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Add levels to the player as much as it is possible.
 | 
			
		||||
        Add as many levels as possible to the player.
 | 
			
		||||
        """
 | 
			
		||||
        while self.current_xp > self.max_xp:
 | 
			
		||||
            self.level += 1
 | 
			
		||||
@@ -59,12 +77,27 @@ class Player(InventoryHolder, FightingEntity):
 | 
			
		||||
 | 
			
		||||
    def add_xp(self, xp: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Add some experience to the player.
 | 
			
		||||
        If the required amount is reached, level up.
 | 
			
		||||
        Adds some experience to the player.
 | 
			
		||||
        If the required amount is reached, the player levels up.
 | 
			
		||||
        """
 | 
			
		||||
        self.current_xp += xp
 | 
			
		||||
        self.current_xp += int(xp * self.xp_buff)
 | 
			
		||||
        self.level_up()
 | 
			
		||||
 | 
			
		||||
    def remove_from_inventory(self, obj: Item) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Remove the given item from the inventory, even if the item is equipped.
 | 
			
		||||
        """
 | 
			
		||||
        if obj == self.equipped_main:
 | 
			
		||||
            self.equipped_main = None
 | 
			
		||||
        elif obj == self.equipped_armor:
 | 
			
		||||
            self.equipped_armor = None
 | 
			
		||||
        elif obj == self.equipped_secondary:
 | 
			
		||||
            self.equipped_secondary = None
 | 
			
		||||
        elif obj == self.equipped_helmet:
 | 
			
		||||
            self.equipped_helmet = None
 | 
			
		||||
        else:
 | 
			
		||||
            return super().remove_from_inventory(obj)
 | 
			
		||||
 | 
			
		||||
    # noinspection PyTypeChecker,PyUnresolvedReferences
 | 
			
		||||
    def check_move(self, y: int, x: int, move_if_possible: bool = False) \
 | 
			
		||||
            -> bool:
 | 
			
		||||
@@ -87,56 +120,6 @@ class Player(InventoryHolder, FightingEntity):
 | 
			
		||||
                    entity.hold(self)
 | 
			
		||||
        return super().check_move(y, x, move_if_possible)
 | 
			
		||||
 | 
			
		||||
    def recalculate_paths(self, max_distance: int = 8) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Use Dijkstra algorithm to calculate best paths for monsters to go to
 | 
			
		||||
        the player. Actually, the paths are computed for each tile adjacent to
 | 
			
		||||
        the player then for each step the monsters use the best path avaliable.
 | 
			
		||||
        """
 | 
			
		||||
        distances = []
 | 
			
		||||
        predecessors = []
 | 
			
		||||
        # four Dijkstras, one for each adjacent tile
 | 
			
		||||
        for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
 | 
			
		||||
            queue = PriorityQueue()
 | 
			
		||||
            new_y, new_x = self.y + dir_y, self.x + dir_x
 | 
			
		||||
            if not 0 <= new_y < self.map.height or \
 | 
			
		||||
                    not 0 <= new_x < self.map.width or \
 | 
			
		||||
                    not self.map.tiles[new_y][new_x].can_walk():
 | 
			
		||||
                continue
 | 
			
		||||
            queue.put(((1, 0), (new_y, new_x)))
 | 
			
		||||
            visited = [(self.y, self.x)]
 | 
			
		||||
            distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
 | 
			
		||||
            predecessors.append({(new_y, new_x): (self.y, self.x)})
 | 
			
		||||
            while not queue.empty():
 | 
			
		||||
                dist, (y, x) = queue.get()
 | 
			
		||||
                if dist[0] >= max_distance or (y, x) in visited:
 | 
			
		||||
                    continue
 | 
			
		||||
                visited.append((y, x))
 | 
			
		||||
                for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
 | 
			
		||||
                    new_y, new_x = y + diff_y, x + diff_x
 | 
			
		||||
                    if not 0 <= new_y < self.map.height or \
 | 
			
		||||
                            not 0 <= new_x < self.map.width or \
 | 
			
		||||
                            not self.map.tiles[new_y][new_x].can_walk():
 | 
			
		||||
                        continue
 | 
			
		||||
                    new_distance = (dist[0] + 1,
 | 
			
		||||
                                    dist[1] + (not self.map.is_free(y, x)))
 | 
			
		||||
                    if not (new_y, new_x) in distances[-1] or \
 | 
			
		||||
                            distances[-1][(new_y, new_x)] > new_distance:
 | 
			
		||||
                        predecessors[-1][(new_y, new_x)] = (y, x)
 | 
			
		||||
                        distances[-1][(new_y, new_x)] = new_distance
 | 
			
		||||
                        queue.put((new_distance, (new_y, new_x)))
 | 
			
		||||
        # For each tile that is reached by at least one Dijkstra, sort the
 | 
			
		||||
        # different paths by distance to the player. For the technical bits :
 | 
			
		||||
        # The reduce function is a fold starting on the first element of the
 | 
			
		||||
        # iterable, and we associate the points to their distance, sort
 | 
			
		||||
        # along the distance, then only keep the points.
 | 
			
		||||
        self.paths = {}
 | 
			
		||||
        for y, x in reduce(set.union,
 | 
			
		||||
                           [set(p.keys()) for p in predecessors], set()):
 | 
			
		||||
            self.paths[(y, x)] = [p for d, p in sorted(
 | 
			
		||||
                [(distances[i][(y, x)], predecessors[i][(y, x)])
 | 
			
		||||
                 for i in range(len(distances)) if (y, x) in predecessors[i]])]
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the state of the entity into a dictionary
 | 
			
		||||
@@ -144,4 +127,12 @@ class Player(InventoryHolder, FightingEntity):
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        d["current_xp"] = self.current_xp
 | 
			
		||||
        d["max_xp"] = self.max_xp
 | 
			
		||||
        d["equipped_main"] = self.equipped_main.save_state()\
 | 
			
		||||
            if self.equipped_main else None
 | 
			
		||||
        d["equipped_armor"] = self.equipped_armor.save_state()\
 | 
			
		||||
            if self.equipped_armor else None
 | 
			
		||||
        d["equipped_secondary"] = self.equipped_secondary.save_state()\
 | 
			
		||||
            if self.equipped_secondary else None
 | 
			
		||||
        d["equipped_helmet"] = self.equipped_helmet.save_state()\
 | 
			
		||||
            if self.equipped_helmet else None
 | 
			
		||||
        return d
 | 
			
		||||
 
 | 
			
		||||
@@ -21,20 +21,20 @@ class DisplayActions(Enum):
 | 
			
		||||
 | 
			
		||||
class GameMode(Enum):
 | 
			
		||||
    """
 | 
			
		||||
    Game mode options
 | 
			
		||||
    Game mode options.
 | 
			
		||||
    """
 | 
			
		||||
    MAINMENU = auto()
 | 
			
		||||
    PLAY = auto()
 | 
			
		||||
    SETTINGS = auto()
 | 
			
		||||
    INVENTORY = auto()
 | 
			
		||||
    STORE = auto()
 | 
			
		||||
    CREDITS = auto()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KeyValues(Enum):
 | 
			
		||||
    """
 | 
			
		||||
    Key values options used in the game
 | 
			
		||||
    Key values options used in the game.
 | 
			
		||||
    """
 | 
			
		||||
    MOUSE = auto()
 | 
			
		||||
    UP = auto()
 | 
			
		||||
    DOWN = auto()
 | 
			
		||||
    LEFT = auto()
 | 
			
		||||
@@ -46,11 +46,13 @@ class KeyValues(Enum):
 | 
			
		||||
    DROP = auto()
 | 
			
		||||
    SPACE = auto()
 | 
			
		||||
    CHAT = auto()
 | 
			
		||||
    WAIT = auto()
 | 
			
		||||
    LADDER = auto()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
 | 
			
		||||
        """
 | 
			
		||||
        Translate the raw string key into an enum value that we can use.
 | 
			
		||||
        Translates the raw string key into an enum value that we can use.
 | 
			
		||||
        """
 | 
			
		||||
        if key in (settings.KEY_DOWN_SECONDARY,
 | 
			
		||||
                   settings.KEY_DOWN_PRIMARY):
 | 
			
		||||
@@ -78,4 +80,8 @@ class KeyValues(Enum):
 | 
			
		||||
            return KeyValues.SPACE
 | 
			
		||||
        elif key == settings.KEY_CHAT:
 | 
			
		||||
            return KeyValues.CHAT
 | 
			
		||||
        elif key == settings.KEY_WAIT:
 | 
			
		||||
            return KeyValues.WAIT
 | 
			
		||||
        elif key == settings.KEY_LADDER:
 | 
			
		||||
            return KeyValues.LADDER
 | 
			
		||||
        return None
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
 | 
			
		||||
from json import JSONDecodeError
 | 
			
		||||
from random import randint
 | 
			
		||||
from typing import Any, Optional
 | 
			
		||||
from typing import Any, Optional, List
 | 
			
		||||
import curses
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
@@ -23,7 +23,8 @@ class Game:
 | 
			
		||||
    """
 | 
			
		||||
    The game object controls all actions in the game.
 | 
			
		||||
    """
 | 
			
		||||
    map: Map
 | 
			
		||||
    maps: List[Map]
 | 
			
		||||
    map_index: int
 | 
			
		||||
    player: Player
 | 
			
		||||
    screen: Any
 | 
			
		||||
    # display_actions is a display interface set by the bootstrapper
 | 
			
		||||
@@ -31,10 +32,11 @@ class Game:
 | 
			
		||||
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Init the game.
 | 
			
		||||
        Initiates the 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()
 | 
			
		||||
@@ -49,8 +51,11 @@ class Game:
 | 
			
		||||
 | 
			
		||||
    def new_game(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Create a new game on the screen.
 | 
			
		||||
        Creates a new game on the screen.
 | 
			
		||||
        """
 | 
			
		||||
        # TODO generate a new map procedurally
 | 
			
		||||
        self.maps = []
 | 
			
		||||
        self.map_index = 0
 | 
			
		||||
        self.map = broguelike.Generator().run()
 | 
			
		||||
        self.map.logs = self.logs
 | 
			
		||||
        self.logs.clear()
 | 
			
		||||
@@ -60,20 +65,44 @@ class Game:
 | 
			
		||||
        self.map.spawn_random_entities(randint(3, 10))
 | 
			
		||||
        self.inventory_menu.update_player(self.player)
 | 
			
		||||
 | 
			
		||||
    def run(self, screen: Any) -> None:
 | 
			
		||||
    @property
 | 
			
		||||
    def map(self) -> Map:
 | 
			
		||||
        """
 | 
			
		||||
        Return the current map where the user is.
 | 
			
		||||
        """
 | 
			
		||||
        return self.maps[self.map_index]
 | 
			
		||||
 | 
			
		||||
    @map.setter
 | 
			
		||||
    def map(self, m: Map) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Redefine the current map.
 | 
			
		||||
        """
 | 
			
		||||
        if len(self.maps) == self.map_index:
 | 
			
		||||
            # Insert new map
 | 
			
		||||
            self.maps.append(m)
 | 
			
		||||
        # Redefine the current map
 | 
			
		||||
        self.maps[self.map_index] = m
 | 
			
		||||
 | 
			
		||||
    def run(self, screen: Any) -> None:  # pragma no cover
 | 
			
		||||
        """
 | 
			
		||||
        Main infinite loop.
 | 
			
		||||
        We wait for the player's action, then we do what that should be done
 | 
			
		||||
        when the given key gets pressed.
 | 
			
		||||
        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)
 | 
			
		||||
            key = screen.getkey()
 | 
			
		||||
            curses.doupdate()
 | 
			
		||||
            try:
 | 
			
		||||
                key = screen.getkey()
 | 
			
		||||
            except KeyboardInterrupt:
 | 
			
		||||
                exit(0)
 | 
			
		||||
                return
 | 
			
		||||
            if key == "KEY_MOUSE":
 | 
			
		||||
                _ignored1, x, y, _ignored2, _ignored3 = curses.getmouse()
 | 
			
		||||
                self.display_actions(DisplayActions.MOUSE, y, x)
 | 
			
		||||
                _ignored1, x, y, _ignored2, attr = curses.getmouse()
 | 
			
		||||
                self.display_actions(DisplayActions.MOUSE, y, x, attr)
 | 
			
		||||
            else:
 | 
			
		||||
                self.handle_key_pressed(
 | 
			
		||||
                    KeyValues.translate_key(key, self.settings), key)
 | 
			
		||||
@@ -81,7 +110,7 @@ class Game:
 | 
			
		||||
    def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
 | 
			
		||||
            -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Indicates what should be done when the given key is pressed,
 | 
			
		||||
        Indicates what should be done when a given key is pressed,
 | 
			
		||||
        according to the current game state.
 | 
			
		||||
        """
 | 
			
		||||
        if self.message:
 | 
			
		||||
@@ -103,36 +132,95 @@ class Game:
 | 
			
		||||
            self.settings_menu.handle_key_pressed(key, raw_key, self)
 | 
			
		||||
        elif self.state == GameMode.STORE:
 | 
			
		||||
            self.handle_key_pressed_store(key)
 | 
			
		||||
        elif self.state == GameMode.CREDITS:
 | 
			
		||||
            self.state = GameMode.MAINMENU
 | 
			
		||||
        self.display_actions(DisplayActions.REFRESH)
 | 
			
		||||
 | 
			
		||||
    def handle_key_pressed_play(self, key: KeyValues) -> None:
 | 
			
		||||
    def handle_key_pressed_play(self, key: KeyValues) -> None:  # noqa: C901
 | 
			
		||||
        """
 | 
			
		||||
        In play mode, arrows or zqsd move the main character.
 | 
			
		||||
        """
 | 
			
		||||
        if key == KeyValues.UP:
 | 
			
		||||
            if self.player.move_up():
 | 
			
		||||
                self.map.tick()
 | 
			
		||||
                self.map.tick(self.player)
 | 
			
		||||
        elif key == KeyValues.DOWN:
 | 
			
		||||
            if self.player.move_down():
 | 
			
		||||
                self.map.tick()
 | 
			
		||||
                self.map.tick(self.player)
 | 
			
		||||
        elif key == KeyValues.LEFT:
 | 
			
		||||
            if self.player.move_left():
 | 
			
		||||
                self.map.tick()
 | 
			
		||||
                self.map.tick(self.player)
 | 
			
		||||
        elif key == KeyValues.RIGHT:
 | 
			
		||||
            if self.player.move_right():
 | 
			
		||||
                self.map.tick()
 | 
			
		||||
                self.map.tick(self.player)
 | 
			
		||||
        elif key == KeyValues.INVENTORY:
 | 
			
		||||
            self.state = GameMode.INVENTORY
 | 
			
		||||
            self.display_actions(DisplayActions.UPDATE)
 | 
			
		||||
        elif key == KeyValues.USE and self.player.equipped_main:
 | 
			
		||||
            if self.player.equipped_main:
 | 
			
		||||
                self.player.equipped_main.use()
 | 
			
		||||
            if self.player.equipped_secondary:
 | 
			
		||||
                self.player.equipped_secondary.use()
 | 
			
		||||
        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(self.player)
 | 
			
		||||
        elif key == KeyValues.LADDER:
 | 
			
		||||
            self.handle_ladder()
 | 
			
		||||
 | 
			
		||||
    def handle_ladder(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        The player pressed the ladder key to switch map
 | 
			
		||||
        """
 | 
			
		||||
        # On a ladder, we switch level
 | 
			
		||||
        y, x = self.player.y, self.player.x
 | 
			
		||||
        if not self.map.tiles[y][x].is_ladder():
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # We move up on the ladder of the beginning,
 | 
			
		||||
        # down at the end of the stage
 | 
			
		||||
        move_down = y != self.map.start_y and x != self.map.start_x
 | 
			
		||||
        old_map = self.map
 | 
			
		||||
        self.map_index += 1 if move_down else -1
 | 
			
		||||
        if self.map_index == -1:
 | 
			
		||||
            self.map_index = 0
 | 
			
		||||
            return
 | 
			
		||||
        while self.map_index >= len(self.maps):
 | 
			
		||||
            # TODO: generate a new map
 | 
			
		||||
            self.maps.append(Map.load(ResourceManager.get_asset_path(
 | 
			
		||||
                "example_map_2.txt")))
 | 
			
		||||
        new_map = self.map
 | 
			
		||||
        new_map.floor = self.map_index
 | 
			
		||||
        old_map.remove_entity(self.player)
 | 
			
		||||
        new_map.add_entity(self.player)
 | 
			
		||||
        if move_down:
 | 
			
		||||
            self.player.move(self.map.start_y, self.map.start_x)
 | 
			
		||||
            self.logs.add_message(
 | 
			
		||||
                _("The player climbs down to the floor {floor}.")
 | 
			
		||||
                .format(floor=-self.map_index))
 | 
			
		||||
        else:
 | 
			
		||||
            # Find the ladder of the end of the game
 | 
			
		||||
            ladder_y, ladder_x = -1, -1
 | 
			
		||||
            for y in range(self.map.height):
 | 
			
		||||
                for x in range(self.map.width):
 | 
			
		||||
                    if (y, x) != (self.map.start_y, self.map.start_x) \
 | 
			
		||||
                            and self.map.tiles[y][x].is_ladder():
 | 
			
		||||
                        ladder_y, ladder_x = y, x
 | 
			
		||||
                        break
 | 
			
		||||
            self.player.move(ladder_y, ladder_x)
 | 
			
		||||
            self.logs.add_message(
 | 
			
		||||
                _("The player climbs up the floor {floor}.")
 | 
			
		||||
                .format(floor=-self.map_index))
 | 
			
		||||
 | 
			
		||||
        self.display_actions(DisplayActions.UPDATE)
 | 
			
		||||
 | 
			
		||||
    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 the player tries to talk to a friendly entity, the game waits for
 | 
			
		||||
        a directional key to be pressed, verifies there is a friendly entity
 | 
			
		||||
        in that direction and then lets the player interact with it.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.waiting_for_friendly_key:
 | 
			
		||||
            return
 | 
			
		||||
@@ -160,7 +248,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:
 | 
			
		||||
        """
 | 
			
		||||
@@ -189,26 +279,37 @@ 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:
 | 
			
		||||
        """
 | 
			
		||||
        In the main menu, we can navigate through options.
 | 
			
		||||
        In the main menu, we can navigate through different options.
 | 
			
		||||
        """
 | 
			
		||||
        if key == KeyValues.DOWN:
 | 
			
		||||
            self.main_menu.go_down()
 | 
			
		||||
@@ -233,16 +334,18 @@ class Game:
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the game to a dictionary
 | 
			
		||||
        Saves the game to a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        return self.map.save_state()
 | 
			
		||||
        return dict(map_index=self.map_index,
 | 
			
		||||
                    maps=[m.save_state() for m in self.maps])
 | 
			
		||||
 | 
			
		||||
    def load_state(self, d: dict) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Loads the game from a dictionary
 | 
			
		||||
        Loads the game from a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            self.map.load_state(d)
 | 
			
		||||
            self.map_index = d["map_index"]
 | 
			
		||||
            self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            self.message = _("Some keys are missing in your save file.\n"
 | 
			
		||||
                             "Your save seems to be corrupt. It got deleted.")
 | 
			
		||||
@@ -259,11 +362,13 @@ class Game:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.player = players[0]
 | 
			
		||||
        self.map.compute_visibility(self.player.y, self.player.x,
 | 
			
		||||
                                    self.player.vision)
 | 
			
		||||
        self.display_actions(DisplayActions.UPDATE)
 | 
			
		||||
 | 
			
		||||
    def load_game(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Loads the game from a file
 | 
			
		||||
        Loads the game from a file.
 | 
			
		||||
        """
 | 
			
		||||
        file_path = ResourceManager.get_config_path("save.json")
 | 
			
		||||
        if os.path.isfile(file_path):
 | 
			
		||||
@@ -280,7 +385,7 @@ class Game:
 | 
			
		||||
 | 
			
		||||
    def save_game(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the game to a file
 | 
			
		||||
        Saves the game to a file.
 | 
			
		||||
        """
 | 
			
		||||
        with open(ResourceManager.get_config_path("save.json"), "w") as f:
 | 
			
		||||
            f.write(json.dumps(self.save_state()))
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,12 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from enum import Enum, auto
 | 
			
		||||
from math import sqrt
 | 
			
		||||
from random import choice, randint
 | 
			
		||||
from typing import List, Optional, Any
 | 
			
		||||
from math import ceil, sqrt
 | 
			
		||||
from itertools import product
 | 
			
		||||
from random import choice, choices, randint
 | 
			
		||||
from typing import List, Optional, Any, Dict, Tuple
 | 
			
		||||
from queue import PriorityQueue
 | 
			
		||||
from functools import reduce
 | 
			
		||||
 | 
			
		||||
from .display.texturepack import TexturePack
 | 
			
		||||
from .translations import gettext as _
 | 
			
		||||
@@ -13,7 +15,7 @@ from .translations import gettext as _
 | 
			
		||||
 | 
			
		||||
class Logs:
 | 
			
		||||
    """
 | 
			
		||||
    The logs object stores the messages to display. It is encapsulating a list
 | 
			
		||||
    The logs object stores the messages to display. It encapsulates a list
 | 
			
		||||
    of such messages, to allow multiple pointers to keep track of it even if
 | 
			
		||||
    the list was to be reassigned.
 | 
			
		||||
    """
 | 
			
		||||
@@ -31,16 +33,47 @@ class Logs:
 | 
			
		||||
        self.messages = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Slope():
 | 
			
		||||
    X: int
 | 
			
		||||
    Y: int
 | 
			
		||||
 | 
			
		||||
    def __init__(self, y: int, x: int) -> None:
 | 
			
		||||
        self.Y = y
 | 
			
		||||
        self.X = x
 | 
			
		||||
 | 
			
		||||
    def compare(self, other: "Slope") -> int:
 | 
			
		||||
        y, x = other.Y, other.X
 | 
			
		||||
        return self.Y * x - self.X * y
 | 
			
		||||
 | 
			
		||||
    def __lt__(self, other: "Slope") -> bool:
 | 
			
		||||
        return self.compare(other) < 0
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other: "Slope") -> bool:
 | 
			
		||||
        return self.compare(other) == 0
 | 
			
		||||
 | 
			
		||||
    def __gt__(self, other: "Slope") -> bool:
 | 
			
		||||
        return self.compare(other) > 0
 | 
			
		||||
 | 
			
		||||
    def __le__(self, other: "Slope") -> bool:
 | 
			
		||||
        return self.compare(other) <= 0
 | 
			
		||||
 | 
			
		||||
    def __ge__(self, other: "Slope") -> bool:
 | 
			
		||||
        return self.compare(other) >= 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Map:
 | 
			
		||||
    """
 | 
			
		||||
    Object that represents a Map with its width, height
 | 
			
		||||
    The Map object represents a with its width, height
 | 
			
		||||
    and tiles, that have their custom properties.
 | 
			
		||||
    """
 | 
			
		||||
    floor: int
 | 
			
		||||
    width: int
 | 
			
		||||
    height: int
 | 
			
		||||
    start_y: int
 | 
			
		||||
    start_x: int
 | 
			
		||||
    tiles: List[List["Tile"]]
 | 
			
		||||
    visibility: List[List[bool]]
 | 
			
		||||
    seen_tiles: List[List[bool]]
 | 
			
		||||
    entities: List["Entity"]
 | 
			
		||||
    logs: Logs
 | 
			
		||||
    # coordinates of the point that should be
 | 
			
		||||
@@ -48,28 +81,36 @@ class Map:
 | 
			
		||||
    currentx: int
 | 
			
		||||
    currenty: int
 | 
			
		||||
 | 
			
		||||
    def __init__(self, width: int, height: int, tiles: list,
 | 
			
		||||
                 start_y: int, start_x: int):
 | 
			
		||||
    def __init__(self, width: int = 0, height: int = 0, tiles: list = None,
 | 
			
		||||
                 start_y: int = 0, start_x: int = 0):
 | 
			
		||||
        self.floor = 0
 | 
			
		||||
        self.width = width
 | 
			
		||||
        self.height = height
 | 
			
		||||
        self.start_y = start_y
 | 
			
		||||
        self.start_x = start_x
 | 
			
		||||
        self.currenty = start_y
 | 
			
		||||
        self.currentx = start_x
 | 
			
		||||
        self.tiles = tiles
 | 
			
		||||
        self.tiles = tiles or []
 | 
			
		||||
        self.visibility = [[False for _ in range(len(self.tiles[0]))]
 | 
			
		||||
                           for _ in range(len(self.tiles))]
 | 
			
		||||
        self.seen_tiles = [[False for _ in range(len(tiles[0]))]
 | 
			
		||||
                           for _ in range(len(self.tiles))]
 | 
			
		||||
        self.entities = []
 | 
			
		||||
        self.logs = Logs()
 | 
			
		||||
 | 
			
		||||
    def add_entity(self, entity: "Entity") -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Register a new entity in the map.
 | 
			
		||||
        Registers a new entity in the map.
 | 
			
		||||
        """
 | 
			
		||||
        self.entities.append(entity)
 | 
			
		||||
        if entity.is_familiar():
 | 
			
		||||
            self.entities.insert(1, entity)
 | 
			
		||||
        else:
 | 
			
		||||
            self.entities.append(entity)
 | 
			
		||||
        entity.map = self
 | 
			
		||||
 | 
			
		||||
    def remove_entity(self, entity: "Entity") -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Unregister an entity from the map.
 | 
			
		||||
        Unregisters an entity from the map.
 | 
			
		||||
        """
 | 
			
		||||
        if entity in self.entities:
 | 
			
		||||
            self.entities.remove(entity)
 | 
			
		||||
@@ -89,7 +130,7 @@ class Map:
 | 
			
		||||
    def entity_is_present(self, y: int, x: int) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Indicates that the tile at the coordinates (y, x) contains a killable
 | 
			
		||||
        entity
 | 
			
		||||
        entity.
 | 
			
		||||
        """
 | 
			
		||||
        return 0 <= y < self.height and 0 <= x < self.width and \
 | 
			
		||||
            any(entity.x == x and entity.y == y and entity.is_friendly()
 | 
			
		||||
@@ -98,7 +139,8 @@ class Map:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def load(filename: str) -> "Map":
 | 
			
		||||
        """
 | 
			
		||||
        Read a file that contains the content of a map, and build a Map object.
 | 
			
		||||
        Reads a file that contains the content of a map,
 | 
			
		||||
        and builds a Map object.
 | 
			
		||||
        """
 | 
			
		||||
        with open(filename, "r") as f:
 | 
			
		||||
            file = f.read()
 | 
			
		||||
@@ -107,7 +149,7 @@ class Map:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def load_from_string(content: str) -> "Map":
 | 
			
		||||
        """
 | 
			
		||||
        Load a map represented by its characters and build a Map object.
 | 
			
		||||
        Loads a map represented by its characters and builds a Map object.
 | 
			
		||||
        """
 | 
			
		||||
        lines = content.split("\n")
 | 
			
		||||
        first_line = lines[0]
 | 
			
		||||
@@ -123,7 +165,7 @@ class Map:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
 | 
			
		||||
        """
 | 
			
		||||
        Transforms a string into the list of corresponding tiles
 | 
			
		||||
        Transforms a string into the list of corresponding tiles.
 | 
			
		||||
        """
 | 
			
		||||
        lines = content.split("\n")
 | 
			
		||||
        tiles = [[Tile.from_ascii_char(c)
 | 
			
		||||
@@ -132,7 +174,7 @@ class Map:
 | 
			
		||||
 | 
			
		||||
    def draw_string(self, pack: TexturePack) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Draw the current map as a string object that can be rendered
 | 
			
		||||
        Draws the current map as a string object that can be rendered
 | 
			
		||||
        in the window.
 | 
			
		||||
        """
 | 
			
		||||
        return "\n".join("".join(tile.char(pack) for tile in line)
 | 
			
		||||
@@ -140,29 +182,153 @@ class Map:
 | 
			
		||||
 | 
			
		||||
    def spawn_random_entities(self, count: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Put randomly {count} entities on the map, where it is available.
 | 
			
		||||
        Puts randomly {count} entities on the map, only on empty ground tiles.
 | 
			
		||||
        """
 | 
			
		||||
        for ignored in range(count):
 | 
			
		||||
        for _ignored in range(count):
 | 
			
		||||
            y, x = 0, 0
 | 
			
		||||
            while True:
 | 
			
		||||
                y, x = randint(0, self.height - 1), randint(0, self.width - 1)
 | 
			
		||||
                tile = self.tiles[y][x]
 | 
			
		||||
                if tile.can_walk():
 | 
			
		||||
                    break
 | 
			
		||||
            entity = choice(Entity.get_all_entity_classes())()
 | 
			
		||||
            entity = choices(Entity.get_all_entity_classes(),
 | 
			
		||||
                             weights=Entity.get_weights(), k=1)[0]()
 | 
			
		||||
            entity.move(y, x)
 | 
			
		||||
            self.add_entity(entity)
 | 
			
		||||
 | 
			
		||||
    def tick(self) -> None:
 | 
			
		||||
    def compute_visibility(self, y: int, x: int, max_range: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Trigger all entity events.
 | 
			
		||||
        Sets the visible tiles to be the ones visible by an entity at point
 | 
			
		||||
        (y, x), using a twaked shadow casting algorithm
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        for line in self.visibility:
 | 
			
		||||
            for i in range(len(line)):
 | 
			
		||||
                line[i] = False
 | 
			
		||||
        self.set_visible(0, 0, 0, (y, x))
 | 
			
		||||
        for octant in range(8):
 | 
			
		||||
            self.compute_visibility_octant(octant, (y, x), max_range, 1,
 | 
			
		||||
                                           Slope(1, 1), Slope(0, 1))
 | 
			
		||||
 | 
			
		||||
    def crop_top_visibility(self, octant: int, origin: Tuple[int, int],
 | 
			
		||||
                            x: int, top: Slope) -> int:
 | 
			
		||||
        if top.X == 1:
 | 
			
		||||
            top_y = x
 | 
			
		||||
        else:
 | 
			
		||||
            top_y = ceil(((x * 2 - 1) * top.Y + top.X) / (top.X * 2))
 | 
			
		||||
            if self.is_wall(top_y, x, octant, origin):
 | 
			
		||||
                top_y += top >= Slope(top_y * 2 + 1, x * 2) and not \
 | 
			
		||||
                    self.is_wall(top_y + 1, x, octant, origin)
 | 
			
		||||
            else:
 | 
			
		||||
                ax = x * 2
 | 
			
		||||
                ax += self.is_wall(top_y + 1, x + 1, octant, origin)
 | 
			
		||||
                top_y += top > Slope(top_y * 2 + 1, ax)
 | 
			
		||||
        return top_y
 | 
			
		||||
 | 
			
		||||
    def crop_bottom_visibility(self, octant: int, origin: Tuple[int, int],
 | 
			
		||||
                               x: int, bottom: Slope) -> int:
 | 
			
		||||
        if bottom.Y == 0:
 | 
			
		||||
            bottom_y = 0
 | 
			
		||||
        else:
 | 
			
		||||
            bottom_y = ceil(((x * 2 - 1) * bottom.Y + bottom.X)
 | 
			
		||||
                            / (bottom.X * 2))
 | 
			
		||||
            bottom_y += bottom >= Slope(bottom_y * 2 + 1, x * 2) and \
 | 
			
		||||
                self.is_wall(bottom_y, x, octant, origin) and \
 | 
			
		||||
                not self.is_wall(bottom_y + 1, x, octant, origin)
 | 
			
		||||
        return bottom_y
 | 
			
		||||
 | 
			
		||||
    def compute_visibility_octant(self, octant: int, origin: Tuple[int, int],
 | 
			
		||||
                                  max_range: int, distance: int, top: Slope,
 | 
			
		||||
                                  bottom: Slope) -> None:
 | 
			
		||||
        for x in range(distance, max_range + 1):
 | 
			
		||||
            top_y = self.crop_top_visibility(octant, origin, x, top)
 | 
			
		||||
            bottom_y = self.crop_bottom_visibility(octant, origin, x, bottom)
 | 
			
		||||
            was_opaque = -1
 | 
			
		||||
            for y in range(top_y, bottom_y - 1, -1):
 | 
			
		||||
                if x + y > max_range:
 | 
			
		||||
                    continue
 | 
			
		||||
                is_opaque = self.is_wall(y, x, octant, origin)
 | 
			
		||||
                is_visible = is_opaque\
 | 
			
		||||
                    or ((y != top_y or top > Slope(y * 4 - 1, x * 4 + 1))
 | 
			
		||||
                        and (y != bottom_y
 | 
			
		||||
                             or bottom < Slope(y * 4 + 1, x * 4 - 1)))
 | 
			
		||||
                # is_visible = is_opaque\
 | 
			
		||||
                #     or ((y != top_y or top >= Slope(y, x))
 | 
			
		||||
                #         and (y != bottom_y or bottom <= Slope(y, x)))
 | 
			
		||||
                if is_visible:
 | 
			
		||||
                    self.set_visible(y, x, octant, origin)
 | 
			
		||||
                if x == max_range:
 | 
			
		||||
                    continue
 | 
			
		||||
                if is_opaque and was_opaque == 0:
 | 
			
		||||
                    nx, ny = x * 2, y * 2 + 1
 | 
			
		||||
                    nx -= self.is_wall(y + 1, x, octant, origin)
 | 
			
		||||
                    if top > Slope(ny, nx):
 | 
			
		||||
                        if y == bottom_y:
 | 
			
		||||
                            bottom = Slope(ny, nx)
 | 
			
		||||
                            break
 | 
			
		||||
                        else:
 | 
			
		||||
                            self.compute_visibility_octant(
 | 
			
		||||
                                octant, origin, max_range, x + 1, top,
 | 
			
		||||
                                Slope(ny, nx))
 | 
			
		||||
                    elif y == bottom_y:  # pragma: no cover
 | 
			
		||||
                        return
 | 
			
		||||
                elif not is_opaque and was_opaque == 1:
 | 
			
		||||
                    nx, ny = x * 2, y * 2 + 1
 | 
			
		||||
                    nx += self.is_wall(y + 1, x + 1, octant, origin)
 | 
			
		||||
                    if bottom >= Slope(ny, nx):  # pragma: no cover
 | 
			
		||||
                        return
 | 
			
		||||
                    top = Slope(ny, nx)
 | 
			
		||||
                was_opaque = is_opaque
 | 
			
		||||
            if was_opaque != 0:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def translate_coord(y: int, x: int, octant: int,
 | 
			
		||||
                        origin: Tuple[int, int]) -> Tuple[int, int]:
 | 
			
		||||
        ny, nx = origin
 | 
			
		||||
        if octant == 0:
 | 
			
		||||
            return ny - y, nx + x
 | 
			
		||||
        elif octant == 1:
 | 
			
		||||
            return ny - x, nx + y
 | 
			
		||||
        elif octant == 2:
 | 
			
		||||
            return ny - x, nx - y
 | 
			
		||||
        elif octant == 3:
 | 
			
		||||
            return ny - y, nx - x
 | 
			
		||||
        elif octant == 4:
 | 
			
		||||
            return ny + y, nx - x
 | 
			
		||||
        elif octant == 5:
 | 
			
		||||
            return ny + x, nx - y
 | 
			
		||||
        elif octant == 6:
 | 
			
		||||
            return ny + x, nx + y
 | 
			
		||||
        elif octant == 7:
 | 
			
		||||
            return ny + y, nx + x
 | 
			
		||||
 | 
			
		||||
    def is_wall(self, y: int, x: int, octant: int,
 | 
			
		||||
                origin: Tuple[int, int]) -> bool:
 | 
			
		||||
        y, x = self.translate_coord(y, x, octant, origin)
 | 
			
		||||
        return 0 <= y < len(self.tiles) and 0 <= x < len(self.tiles[0]) and \
 | 
			
		||||
            self.tiles[y][x].is_wall()
 | 
			
		||||
 | 
			
		||||
    def set_visible(self, y: int, x: int, octant: int,
 | 
			
		||||
                    origin: Tuple[int, int]) -> None:
 | 
			
		||||
        y, x = self.translate_coord(y, x, octant, origin)
 | 
			
		||||
        if 0 <= y < len(self.tiles) and 0 <= x < len(self.tiles[0]):
 | 
			
		||||
            self.visibility[y][x] = True
 | 
			
		||||
            self.seen_tiles[y][x] = True
 | 
			
		||||
 | 
			
		||||
    def tick(self, p: Any) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Triggers all entity events.
 | 
			
		||||
        """
 | 
			
		||||
        for entity in self.entities:
 | 
			
		||||
            entity.act(self)
 | 
			
		||||
            if entity.is_familiar():
 | 
			
		||||
                entity.act(p, self)
 | 
			
		||||
            else:
 | 
			
		||||
                entity.act(self)
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the map's attributes to a dictionary
 | 
			
		||||
        Saves the map's attributes to a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        d = dict()
 | 
			
		||||
        d["width"] = self.width
 | 
			
		||||
@@ -175,11 +341,12 @@ class Map:
 | 
			
		||||
        for enti in self.entities:
 | 
			
		||||
            d["entities"].append(enti.save_state())
 | 
			
		||||
        d["map"] = self.draw_string(TexturePack.ASCII_PACK)
 | 
			
		||||
        d["seen_tiles"] = self.seen_tiles
 | 
			
		||||
        return d
 | 
			
		||||
 | 
			
		||||
    def load_state(self, d: dict) -> None:
 | 
			
		||||
    def load_state(self, d: dict) -> "Map":
 | 
			
		||||
        """
 | 
			
		||||
        Loads the map's attributes from a dictionary
 | 
			
		||||
        Loads the map's attributes from a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        self.width = d["width"]
 | 
			
		||||
        self.height = d["height"]
 | 
			
		||||
@@ -188,11 +355,16 @@ class Map:
 | 
			
		||||
        self.currentx = d["currentx"]
 | 
			
		||||
        self.currenty = d["currenty"]
 | 
			
		||||
        self.tiles = self.load_dungeon_from_string(d["map"])
 | 
			
		||||
        self.seen_tiles = d["seen_tiles"]
 | 
			
		||||
        self.visibility = [[False for _ in range(len(self.tiles[0]))]
 | 
			
		||||
                           for _ in range(len(self.tiles))]
 | 
			
		||||
        self.entities = []
 | 
			
		||||
        dictclasses = Entity.get_all_entity_classes_in_a_dict()
 | 
			
		||||
        for entisave in d["entities"]:
 | 
			
		||||
            self.add_entity(dictclasses[entisave["type"]](**entisave))
 | 
			
		||||
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def neighbourhood(grid: List[List["Tile"]], y: int, x: int,
 | 
			
		||||
                      large: bool = False, oob: bool = False) \
 | 
			
		||||
@@ -217,16 +389,17 @@ class Map:
 | 
			
		||||
 | 
			
		||||
class Tile(Enum):
 | 
			
		||||
    """
 | 
			
		||||
    The internal representation of the tiles of the map
 | 
			
		||||
    The internal representation of the tiles of the map.
 | 
			
		||||
    """
 | 
			
		||||
    EMPTY = auto()
 | 
			
		||||
    WALL = auto()
 | 
			
		||||
    FLOOR = auto()
 | 
			
		||||
    LADDER = auto()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def from_ascii_char(ch: str) -> "Tile":
 | 
			
		||||
        """
 | 
			
		||||
        Maps an ascii character to its equivalent in the texture pack
 | 
			
		||||
        Maps an ascii character to its equivalent in the texture pack.
 | 
			
		||||
        """
 | 
			
		||||
        for tile in Tile:
 | 
			
		||||
            if tile.char(TexturePack.ASCII_PACK) == ch:
 | 
			
		||||
@@ -236,9 +409,27 @@ class Tile(Enum):
 | 
			
		||||
    def char(self, pack: TexturePack) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Translates a Tile to the corresponding character according
 | 
			
		||||
        to the texture pack
 | 
			
		||||
        to the texture pack.
 | 
			
		||||
        """
 | 
			
		||||
        return getattr(pack, self.name)
 | 
			
		||||
        val = getattr(pack, self.name)
 | 
			
		||||
        return val[0] if isinstance(val, tuple) else val
 | 
			
		||||
 | 
			
		||||
    def visible_color(self, pack: TexturePack) -> Tuple[int, int]:
 | 
			
		||||
        """
 | 
			
		||||
        Retrieve the tuple (fg_color, bg_color) of the current Tile
 | 
			
		||||
        if it is visible.
 | 
			
		||||
        """
 | 
			
		||||
        val = getattr(pack, self.name)
 | 
			
		||||
        return (val[2], val[4]) if isinstance(val, tuple) else \
 | 
			
		||||
            (pack.tile_fg_visible_color, pack.tile_bg_color)
 | 
			
		||||
 | 
			
		||||
    def hidden_color(self, pack: TexturePack) -> Tuple[int, int]:
 | 
			
		||||
        """
 | 
			
		||||
        Retrieve the tuple (fg_color, bg_color) of the current Tile.
 | 
			
		||||
        """
 | 
			
		||||
        val = getattr(pack, self.name)
 | 
			
		||||
        return (val[1], val[3]) if isinstance(val, tuple) else \
 | 
			
		||||
            (pack.tile_fg_color, pack.tile_bg_color)
 | 
			
		||||
 | 
			
		||||
    def is_wall(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
@@ -246,21 +437,28 @@ class Tile(Enum):
 | 
			
		||||
        """
 | 
			
		||||
        return self == Tile.WALL
 | 
			
		||||
 | 
			
		||||
    def is_ladder(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Is this Tile a ladder?
 | 
			
		||||
        """
 | 
			
		||||
        return self == Tile.LADDER
 | 
			
		||||
 | 
			
		||||
    def can_walk(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Check if an entity (player or not) can move in this tile.
 | 
			
		||||
        Checks if an entity (player or not) can move in this tile.
 | 
			
		||||
        """
 | 
			
		||||
        return not self.is_wall() and self != Tile.EMPTY
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Entity:
 | 
			
		||||
    """
 | 
			
		||||
    An Entity object represents any entity present on the map
 | 
			
		||||
    An Entity object represents any entity present on the map.
 | 
			
		||||
    """
 | 
			
		||||
    y: int
 | 
			
		||||
    x: int
 | 
			
		||||
    name: str
 | 
			
		||||
    map: Map
 | 
			
		||||
    paths: Dict[Tuple[int, int], Tuple[int, int]]
 | 
			
		||||
 | 
			
		||||
    # noinspection PyShadowingBuiltins
 | 
			
		||||
    def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
 | 
			
		||||
@@ -269,11 +467,12 @@ class Entity:
 | 
			
		||||
        self.x = x
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.map = map
 | 
			
		||||
        self.paths = None
 | 
			
		||||
 | 
			
		||||
    def check_move(self, y: int, x: int, move_if_possible: bool = False)\
 | 
			
		||||
            -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Checks if moving to (y,x) is authorized
 | 
			
		||||
        Checks if moving to (y,x) is authorized.
 | 
			
		||||
        """
 | 
			
		||||
        free = self.map.is_free(y, x)
 | 
			
		||||
        if free and move_if_possible:
 | 
			
		||||
@@ -282,7 +481,7 @@ class Entity:
 | 
			
		||||
 | 
			
		||||
    def move(self, y: int, x: int) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Moves an entity to (y,x) coordinates
 | 
			
		||||
        Moves an entity to (y,x) coordinates.
 | 
			
		||||
        """
 | 
			
		||||
        self.y = y
 | 
			
		||||
        self.x = x
 | 
			
		||||
@@ -290,49 +489,100 @@ class Entity:
 | 
			
		||||
 | 
			
		||||
    def move_up(self, force: bool = False) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Moves the entity up one tile, if possible
 | 
			
		||||
        Moves the entity up one tile, if possible.
 | 
			
		||||
        """
 | 
			
		||||
        return self.move(self.y - 1, self.x) if force else \
 | 
			
		||||
            self.check_move(self.y - 1, self.x, True)
 | 
			
		||||
 | 
			
		||||
    def move_down(self, force: bool = False) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Moves the entity down one tile, if possible
 | 
			
		||||
        Moves the entity down one tile, if possible.
 | 
			
		||||
        """
 | 
			
		||||
        return self.move(self.y + 1, self.x) if force else \
 | 
			
		||||
            self.check_move(self.y + 1, self.x, True)
 | 
			
		||||
 | 
			
		||||
    def move_left(self, force: bool = False) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Moves the entity left one tile, if possible
 | 
			
		||||
        Moves the entity left one tile, if possible.
 | 
			
		||||
        """
 | 
			
		||||
        return self.move(self.y, self.x - 1) if force else \
 | 
			
		||||
            self.check_move(self.y, self.x - 1, True)
 | 
			
		||||
 | 
			
		||||
    def move_right(self, force: bool = False) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Moves the entity right one tile, if possible
 | 
			
		||||
        Moves the entity right one tile, if possible.
 | 
			
		||||
        """
 | 
			
		||||
        return self.move(self.y, self.x + 1) if force else \
 | 
			
		||||
            self.check_move(self.y, self.x + 1, True)
 | 
			
		||||
 | 
			
		||||
    def recalculate_paths(self, max_distance: int = 12) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Uses Dijkstra algorithm to calculate best paths for other entities to
 | 
			
		||||
        go to this entity. If self.paths is None, does nothing.
 | 
			
		||||
        """
 | 
			
		||||
        if self.paths is None:
 | 
			
		||||
            return
 | 
			
		||||
        distances = []
 | 
			
		||||
        predecessors = []
 | 
			
		||||
        # four Dijkstras, one for each adjacent tile
 | 
			
		||||
        for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
 | 
			
		||||
            queue = PriorityQueue()
 | 
			
		||||
            new_y, new_x = self.y + dir_y, self.x + dir_x
 | 
			
		||||
            if not 0 <= new_y < self.map.height or \
 | 
			
		||||
                    not 0 <= new_x < self.map.width or \
 | 
			
		||||
                    not self.map.tiles[new_y][new_x].can_walk():
 | 
			
		||||
                continue
 | 
			
		||||
            queue.put(((1, 0), (new_y, new_x)))
 | 
			
		||||
            visited = [(self.y, self.x)]
 | 
			
		||||
            distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
 | 
			
		||||
            predecessors.append({(new_y, new_x): (self.y, self.x)})
 | 
			
		||||
            while not queue.empty():
 | 
			
		||||
                dist, (y, x) = queue.get()
 | 
			
		||||
                if dist[0] >= max_distance or (y, x) in visited:
 | 
			
		||||
                    continue
 | 
			
		||||
                visited.append((y, x))
 | 
			
		||||
                for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
 | 
			
		||||
                    new_y, new_x = y + diff_y, x + diff_x
 | 
			
		||||
                    if not 0 <= new_y < self.map.height or \
 | 
			
		||||
                            not 0 <= new_x < self.map.width or \
 | 
			
		||||
                            not self.map.tiles[new_y][new_x].can_walk():
 | 
			
		||||
                        continue
 | 
			
		||||
                    new_distance = (dist[0] + 1,
 | 
			
		||||
                                    dist[1] + (not self.map.is_free(y, x)))
 | 
			
		||||
                    if not (new_y, new_x) in distances[-1] or \
 | 
			
		||||
                            distances[-1][(new_y, new_x)] > new_distance:
 | 
			
		||||
                        predecessors[-1][(new_y, new_x)] = (y, x)
 | 
			
		||||
                        distances[-1][(new_y, new_x)] = new_distance
 | 
			
		||||
                        queue.put((new_distance, (new_y, new_x)))
 | 
			
		||||
        # For each tile that is reached by at least one Dijkstra, sort the
 | 
			
		||||
        # different paths by distance to the player. For the technical bits :
 | 
			
		||||
        # The reduce function is a fold starting on the first element of the
 | 
			
		||||
        # iterable, and we associate the points to their distance, sort
 | 
			
		||||
        # along the distance, then only keep the points.
 | 
			
		||||
        self.paths = {}
 | 
			
		||||
        for y, x in reduce(set.union,
 | 
			
		||||
                           [set(p.keys()) for p in predecessors], set()):
 | 
			
		||||
            self.paths[(y, x)] = [p for d, p in sorted(
 | 
			
		||||
                [(distances[i][(y, x)], predecessors[i][(y, x)])
 | 
			
		||||
                 for i in range(len(distances)) if (y, x) in predecessors[i]])]
 | 
			
		||||
 | 
			
		||||
    def act(self, m: Map) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Define the action of the entity that is ran each tick.
 | 
			
		||||
        Defines the action the entity will do at each tick.
 | 
			
		||||
        By default, does nothing.
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def distance_squared(self, other: "Entity") -> int:
 | 
			
		||||
        """
 | 
			
		||||
        Get the square of the distance to another entity.
 | 
			
		||||
        Useful to check distances since square root takes time.
 | 
			
		||||
        Gives the square of the distance to another entity.
 | 
			
		||||
        Useful to check distances since taking the square root takes time.
 | 
			
		||||
        """
 | 
			
		||||
        return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
 | 
			
		||||
 | 
			
		||||
    def distance(self, other: "Entity") -> float:
 | 
			
		||||
        """
 | 
			
		||||
        Get the cartesian distance to another entity.
 | 
			
		||||
        Gives the cartesian distance to another entity.
 | 
			
		||||
        """
 | 
			
		||||
        return sqrt(self.distance_squared(other))
 | 
			
		||||
 | 
			
		||||
@@ -355,6 +605,13 @@ class Entity:
 | 
			
		||||
        """
 | 
			
		||||
        return isinstance(self, FriendlyEntity)
 | 
			
		||||
 | 
			
		||||
    def is_familiar(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Is this entity a familiar?
 | 
			
		||||
        """
 | 
			
		||||
        from squirrelbattle.entities.friendly import Familiar
 | 
			
		||||
        return isinstance(self, Familiar)
 | 
			
		||||
 | 
			
		||||
    def is_merchant(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Is this entity a merchant?
 | 
			
		||||
@@ -364,48 +621,71 @@ class Entity:
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def translated_name(self) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Translates the name of entities.
 | 
			
		||||
        """
 | 
			
		||||
        return _(self.name.replace("_", " "))
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_entity_classes() -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Returns all entities subclasses
 | 
			
		||||
        Returns all entities subclasses.
 | 
			
		||||
        """
 | 
			
		||||
        from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart
 | 
			
		||||
        from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
 | 
			
		||||
            Rabbit, TeddyBear
 | 
			
		||||
        from squirrelbattle.entities.friendly import Merchant, Sunflower
 | 
			
		||||
            Rabbit, TeddyBear, GiantSeaEagle
 | 
			
		||||
        from squirrelbattle.entities.friendly import Merchant, Sunflower, \
 | 
			
		||||
            Trumpet
 | 
			
		||||
        return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,
 | 
			
		||||
                Sunflower, Tiger, Merchant]
 | 
			
		||||
                Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet]
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_weights() -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Returns a weigth list associated to the above function, to
 | 
			
		||||
        be used to spawn random entities with a certain probability.
 | 
			
		||||
        """
 | 
			
		||||
        return [3, 5, 6, 5, 5, 5,
 | 
			
		||||
                5, 4, 4, 1, 2]
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_all_entity_classes_in_a_dict() -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Returns all entities subclasses in a dictionary
 | 
			
		||||
        Returns all entities subclasses in a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        from squirrelbattle.entities.player import Player
 | 
			
		||||
        from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
 | 
			
		||||
            TeddyBear
 | 
			
		||||
        from squirrelbattle.entities.friendly import Merchant, Sunflower
 | 
			
		||||
            TeddyBear, GiantSeaEagle
 | 
			
		||||
        from squirrelbattle.entities.friendly import Merchant, Sunflower, \
 | 
			
		||||
            Trumpet
 | 
			
		||||
        from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \
 | 
			
		||||
            Heart, Sword
 | 
			
		||||
            Heart, Monocle, Sword, Shield, Chestplate, Helmet, \
 | 
			
		||||
            RingCritical, RingXP
 | 
			
		||||
        return {
 | 
			
		||||
            "Tiger": Tiger,
 | 
			
		||||
            "Bomb": Bomb,
 | 
			
		||||
            "Chestplate": Chestplate,
 | 
			
		||||
            "Heart": Heart,
 | 
			
		||||
            "BodySnatchPotion": BodySnatchPotion,
 | 
			
		||||
            "Eagle": GiantSeaEagle,
 | 
			
		||||
            "Hedgehog": Hedgehog,
 | 
			
		||||
            "Rabbit": Rabbit,
 | 
			
		||||
            "TeddyBear": TeddyBear,
 | 
			
		||||
            "Helmet": Helmet,
 | 
			
		||||
            "Player": Player,
 | 
			
		||||
            "Merchant": Merchant,
 | 
			
		||||
            "Monocle": Monocle,
 | 
			
		||||
            "Sunflower": Sunflower,
 | 
			
		||||
            "Sword": Sword,
 | 
			
		||||
            "Trumpet": Trumpet,
 | 
			
		||||
            "Shield": Shield,
 | 
			
		||||
            "TeddyBear": TeddyBear,
 | 
			
		||||
            "Tiger": Tiger,
 | 
			
		||||
            "Rabbit": Rabbit,
 | 
			
		||||
            "RingCritical": RingCritical,
 | 
			
		||||
            "RingXP": RingXP,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the coordinates of the entity
 | 
			
		||||
        Saves the coordinates of the entity.
 | 
			
		||||
        """
 | 
			
		||||
        d = dict()
 | 
			
		||||
        d["x"] = self.x
 | 
			
		||||
@@ -417,7 +697,7 @@ class Entity:
 | 
			
		||||
class FightingEntity(Entity):
 | 
			
		||||
    """
 | 
			
		||||
    A FightingEntity is an entity that can fight, and thus has a health,
 | 
			
		||||
    level and stats
 | 
			
		||||
    level and stats.
 | 
			
		||||
    """
 | 
			
		||||
    maxhealth: int
 | 
			
		||||
    health: int
 | 
			
		||||
@@ -427,11 +707,12 @@ class FightingEntity(Entity):
 | 
			
		||||
    dexterity: int
 | 
			
		||||
    constitution: int
 | 
			
		||||
    level: int
 | 
			
		||||
    critical: int
 | 
			
		||||
 | 
			
		||||
    def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
 | 
			
		||||
                 strength: int = 0, intelligence: int = 0, charisma: int = 0,
 | 
			
		||||
                 dexterity: int = 0, constitution: int = 0, level: int = 0,
 | 
			
		||||
                 *args, **kwargs) -> None:
 | 
			
		||||
                 critical: int = 0, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.maxhealth = maxhealth
 | 
			
		||||
        self.health = maxhealth if health is None else health
 | 
			
		||||
@@ -441,49 +722,62 @@ class FightingEntity(Entity):
 | 
			
		||||
        self.dexterity = dexterity
 | 
			
		||||
        self.constitution = constitution
 | 
			
		||||
        self.level = level
 | 
			
		||||
        self.critical = critical
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def dead(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Is this entity dead ?
 | 
			
		||||
        """
 | 
			
		||||
        return self.health <= 0
 | 
			
		||||
 | 
			
		||||
    def hit(self, opponent: "FightingEntity") -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Deals damage to the opponent, based on the stats
 | 
			
		||||
        The entity deals damage to the opponent
 | 
			
		||||
        based on their respective stats.
 | 
			
		||||
        """
 | 
			
		||||
        diceroll = randint(1, 100)
 | 
			
		||||
        damage = self.strength
 | 
			
		||||
        string = " "
 | 
			
		||||
        if diceroll <= self.critical:  # It is a critical hit
 | 
			
		||||
            damage *= 4
 | 
			
		||||
            string = " " + _("It's a critical hit!") + " "
 | 
			
		||||
        return _("{name} hits {opponent}.")\
 | 
			
		||||
            .format(name=_(self.translated_name.capitalize()),
 | 
			
		||||
                    opponent=_(opponent.translated_name)) + " " + \
 | 
			
		||||
            opponent.take_damage(self, self.strength)
 | 
			
		||||
                    opponent=_(opponent.translated_name)) + string + \
 | 
			
		||||
            opponent.take_damage(self, damage)
 | 
			
		||||
 | 
			
		||||
    def take_damage(self, attacker: "Entity", amount: int) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Take damage from the attacker, based on the stats
 | 
			
		||||
        The entity takes damage from the attacker
 | 
			
		||||
        based on their respective stats.
 | 
			
		||||
        """
 | 
			
		||||
        self.health -= amount
 | 
			
		||||
        damage = max(0, amount - self.constitution)
 | 
			
		||||
        self.health -= damage
 | 
			
		||||
        if self.health <= 0:
 | 
			
		||||
            self.die()
 | 
			
		||||
        return _("{name} takes {amount} damage.")\
 | 
			
		||||
            .format(name=self.translated_name.capitalize(), amount=str(amount))\
 | 
			
		||||
        return _("{name} takes {damage} damage.")\
 | 
			
		||||
            .format(name=self.translated_name.capitalize(), damage=str(damage))\
 | 
			
		||||
            + (" " + _("{name} dies.")
 | 
			
		||||
               .format(name=self.translated_name.capitalize())
 | 
			
		||||
               if self.health <= 0 else "")
 | 
			
		||||
 | 
			
		||||
    def die(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        If a fighting entity has no more health, it dies and is removed
 | 
			
		||||
        If a fighting entity has no more health, it dies and is removed.
 | 
			
		||||
        """
 | 
			
		||||
        self.map.remove_entity(self)
 | 
			
		||||
 | 
			
		||||
    def keys(self) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Returns a fighting entity's specific attributes
 | 
			
		||||
        Returns a fighting entity's specific attributes.
 | 
			
		||||
        """
 | 
			
		||||
        return ["name", "maxhealth", "health", "level", "strength",
 | 
			
		||||
                "intelligence", "charisma", "dexterity", "constitution"]
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Saves the state of the entity into a dictionary
 | 
			
		||||
        Saves the state of the entity into a dictionary.
 | 
			
		||||
        """
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        for name in self.keys():
 | 
			
		||||
@@ -493,18 +787,18 @@ class FightingEntity(Entity):
 | 
			
		||||
 | 
			
		||||
class FriendlyEntity(FightingEntity):
 | 
			
		||||
    """
 | 
			
		||||
    Friendly entities are living entities which do not attack the player
 | 
			
		||||
    Friendly entities are living entities which do not attack the player.
 | 
			
		||||
    """
 | 
			
		||||
    dialogue_option: list
 | 
			
		||||
 | 
			
		||||
    def talk_to(self, player: Any) -> str:
 | 
			
		||||
        a = randint(0, len(self.dialogue_option) - 1)
 | 
			
		||||
        return "The " + self.translated_name \
 | 
			
		||||
               + " said : " + self.dialogue_option[a]
 | 
			
		||||
        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
 | 
			
		||||
        Returns a friendly entity's specific attributes.
 | 
			
		||||
        """
 | 
			
		||||
        return ["maxhealth", "health"]
 | 
			
		||||
 | 
			
		||||
@@ -515,17 +809,17 @@ class InventoryHolder(Entity):
 | 
			
		||||
 | 
			
		||||
    def translate_inventory(self, inventory: list) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Translate the JSON-state of the inventory into a list of the items in
 | 
			
		||||
        Translates the JSON save 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])
 | 
			
		||||
                inventory[i] = self.dict_to_item(inventory[i])
 | 
			
		||||
        return inventory
 | 
			
		||||
 | 
			
		||||
    def dict_to_inventory(self, item_dict: dict) -> Entity:
 | 
			
		||||
    def dict_to_item(self, item_dict: dict) -> Entity:
 | 
			
		||||
        """
 | 
			
		||||
        Translate a dict object that contains the state of an item
 | 
			
		||||
        Translates a dictionnary that contains the state of an item
 | 
			
		||||
        into an item object.
 | 
			
		||||
        """
 | 
			
		||||
        entity_classes = self.get_all_entity_classes_in_a_dict()
 | 
			
		||||
@@ -535,7 +829,7 @@ class InventoryHolder(Entity):
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        We save the inventory of the merchant formatted as JSON
 | 
			
		||||
        The inventory of the merchant is saved in a JSON format.
 | 
			
		||||
        """
 | 
			
		||||
        d = super().save_state()
 | 
			
		||||
        d["hazel"] = self.hazel
 | 
			
		||||
@@ -544,19 +838,21 @@ class InventoryHolder(Entity):
 | 
			
		||||
 | 
			
		||||
    def add_to_inventory(self, obj: Any) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Adds an object to inventory
 | 
			
		||||
        Adds an object to the inventory.
 | 
			
		||||
        """
 | 
			
		||||
        self.inventory.append(obj)
 | 
			
		||||
        if obj not in self.inventory:
 | 
			
		||||
            self.inventory.append(obj)
 | 
			
		||||
 | 
			
		||||
    def remove_from_inventory(self, obj: Any) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Removes an object from the inventory
 | 
			
		||||
        Removes an object from the inventory.
 | 
			
		||||
        """
 | 
			
		||||
        self.inventory.remove(obj)
 | 
			
		||||
        if obj in self.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
 | 
			
		||||
        Changes the number of hazel the entity has by hz. hz is negative
 | 
			
		||||
        when the entity loses money and positive when it gains money.
 | 
			
		||||
        """
 | 
			
		||||
        self.hazel += hz
 | 
			
		||||
 
 | 
			
		||||
@@ -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 <EMAIL@ADDRESS>, 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-11 18:06+0100\n"
 | 
			
		||||
"POT-Creation-Date: 2021-01-08 15:15+0100\n"
 | 
			
		||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
			
		||||
@@ -15,52 +17,116 @@ msgstr ""
 | 
			
		||||
"Content-Type: text/plain; charset=UTF-8\n"
 | 
			
		||||
"Content-Transfer-Encoding: 8bit\n"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:113
 | 
			
		||||
msgid "== INVENTORY =="
 | 
			
		||||
msgstr "== BESTAND =="
 | 
			
		||||
msgid "ring_of_critical_damage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:134
 | 
			
		||||
msgid "== STALL =="
 | 
			
		||||
msgstr "== STAND =="
 | 
			
		||||
msgid "ring_of_more_experience"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:34
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} takes {amount} damage."
 | 
			
		||||
msgstr "{name} nimmt {amount} Schadenspunkte."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:28
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:123
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:148
 | 
			
		||||
msgid "Credits"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:32
 | 
			
		||||
msgid "Developers:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:38
 | 
			
		||||
msgid "Translators:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:168
 | 
			
		||||
msgid "INVENTORY"
 | 
			
		||||
msgstr "BESTAND"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:214
 | 
			
		||||
msgid "STALL"
 | 
			
		||||
msgstr "STAND"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:44
 | 
			
		||||
msgid "Inventory:"
 | 
			
		||||
msgstr "Bestand:"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:53
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:61
 | 
			
		||||
msgid "Equipped main:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:65
 | 
			
		||||
msgid "Equipped secondary:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:70
 | 
			
		||||
msgid "Equipped chestplate:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:74
 | 
			
		||||
msgid "Equipped helmet:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:81
 | 
			
		||||
msgid "YOU ARE DEAD"
 | 
			
		||||
msgstr "SIE WURDEN GESTORBEN"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:85
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "Use {key} to use the ladder"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:94
 | 
			
		||||
msgid "Move to the friendly entity to talk to it"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:96
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "Use {key} then move to talk to the entity"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#. TODO
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:33
 | 
			
		||||
msgid "I don't sell any squirrel"
 | 
			
		||||
msgstr "Ich verkaufe keinen Eichhörnchen."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:46
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:55
 | 
			
		||||
msgid "Flower power!!"
 | 
			
		||||
msgstr "Blumenmacht!!"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:46
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:55
 | 
			
		||||
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:151
 | 
			
		||||
#: squirrelbattle/entities/items.py:178
 | 
			
		||||
msgid "Bomb is exploding."
 | 
			
		||||
msgstr "Die Bombe explodiert."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/items.py:224
 | 
			
		||||
#: squirrelbattle/entities/items.py:365
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{player} exchanged its body with {entity}."
 | 
			
		||||
msgstr "{player} täuscht seinem Körper mit {entity} aus."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:199 squirrelbattle/tests/game_test.py:537
 | 
			
		||||
msgid "You do not have enough money"
 | 
			
		||||
msgstr ""
 | 
			
		||||
#: squirrelbattle/game.py:200
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "The player climbs down to the floor {floor}."
 | 
			
		||||
msgstr "Der Spieler klettert auf dem Stock {floor} hinunter."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:243
 | 
			
		||||
#: squirrelbattle/game.py:213
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "The player climbs up the floor {floor}."
 | 
			
		||||
msgstr "Der Spieler klettert auf dem Stock {floor} hinoben."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:304 squirrelbattle/tests/game_test.py:603
 | 
			
		||||
msgid "The buyer does not have enough money"
 | 
			
		||||
msgstr "Der Kaufer hat nicht genug Geld"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:349
 | 
			
		||||
msgid ""
 | 
			
		||||
"Some keys are missing in your save file.\n"
 | 
			
		||||
"Your save seems to be corrupt. It got deleted."
 | 
			
		||||
@@ -68,7 +134,7 @@ msgstr ""
 | 
			
		||||
"In Ihrer Speicherdatei fehlen einige Schlüssel.\n"
 | 
			
		||||
"Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:251
 | 
			
		||||
#: squirrelbattle/game.py:357
 | 
			
		||||
msgid ""
 | 
			
		||||
"No player was found on this map!\n"
 | 
			
		||||
"Maybe you died?"
 | 
			
		||||
@@ -76,7 +142,7 @@ msgstr ""
 | 
			
		||||
"Auf dieser Karte wurde kein Spieler gefunden!\n"
 | 
			
		||||
"Vielleicht sind Sie gestorben?"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:271
 | 
			
		||||
#: squirrelbattle/game.py:379
 | 
			
		||||
msgid ""
 | 
			
		||||
"The JSON file is not correct.\n"
 | 
			
		||||
"Your save seems corrupted. It got deleted."
 | 
			
		||||
@@ -84,27 +150,36 @@ msgstr ""
 | 
			
		||||
"Die JSON-Datei ist nicht korrekt.\n"
 | 
			
		||||
"Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:429
 | 
			
		||||
#: squirrelbattle/interfaces.py:718
 | 
			
		||||
msgid "It's a critical hit!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:719
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} hits {opponent}."
 | 
			
		||||
msgstr "{name} schlägt {opponent}."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:441
 | 
			
		||||
#: squirrelbattle/interfaces.py:733
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} takes {amount} damage."
 | 
			
		||||
msgstr "{name} nimmt {amount} Schadenspunkte."
 | 
			
		||||
msgid "{name} takes {damage} damage."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:443
 | 
			
		||||
#: squirrelbattle/interfaces.py:735
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} dies."
 | 
			
		||||
msgstr "{name} stirbt."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:769
 | 
			
		||||
#, 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:314 squirrelbattle/tests/game_test.py:317
 | 
			
		||||
#: squirrelbattle/tests/game_test.py:320
 | 
			
		||||
#: squirrelbattle/tests/game_test.py:368 squirrelbattle/tests/game_test.py:371
 | 
			
		||||
#: squirrelbattle/tests/game_test.py:374 squirrelbattle/tests/game_test.py:377
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:16
 | 
			
		||||
msgid "New game"
 | 
			
		||||
msgstr "Neu Spiel"
 | 
			
		||||
@@ -186,45 +261,93 @@ 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 "Key used to use ladders"
 | 
			
		||||
msgstr "Leitertaste"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:58
 | 
			
		||||
msgid "Texture pack"
 | 
			
		||||
msgstr "Textur-Packung"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:56
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:59
 | 
			
		||||
msgid "Language"
 | 
			
		||||
msgstr "Sprache"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:59
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:62
 | 
			
		||||
msgid "player"
 | 
			
		||||
msgstr "Spieler"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:61
 | 
			
		||||
msgid "tiger"
 | 
			
		||||
msgstr "Tiger"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:62
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:64
 | 
			
		||||
msgid "hedgehog"
 | 
			
		||||
msgstr "Igel"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:63
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:65
 | 
			
		||||
msgid "merchant"
 | 
			
		||||
msgstr "Kaufmann"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:66
 | 
			
		||||
msgid "rabbit"
 | 
			
		||||
msgstr "Kanninchen"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:64
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:67
 | 
			
		||||
msgid "sunflower"
 | 
			
		||||
msgstr "Sonnenblume"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:68
 | 
			
		||||
msgid "teddy bear"
 | 
			
		||||
msgstr "Teddybär"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:66
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:69
 | 
			
		||||
msgid "tiger"
 | 
			
		||||
msgstr "Tiger"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:70
 | 
			
		||||
msgid "eagle"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:72
 | 
			
		||||
msgid "body snatch potion"
 | 
			
		||||
msgstr "Leichenfleddererzaubertrank"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:67
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:73
 | 
			
		||||
msgid "bomb"
 | 
			
		||||
msgstr "Bombe"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:68
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:74
 | 
			
		||||
msgid "explosion"
 | 
			
		||||
msgstr "Explosion"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:75
 | 
			
		||||
msgid "heart"
 | 
			
		||||
msgstr "Herz"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:69
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:76
 | 
			
		||||
msgid "sword"
 | 
			
		||||
msgstr "schwert"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:77
 | 
			
		||||
msgid "helmet"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:78
 | 
			
		||||
msgid "chestplate"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:79
 | 
			
		||||
msgid "shield"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:80
 | 
			
		||||
msgid "ring of critical damage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:82
 | 
			
		||||
msgid "ring of more experience"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:84
 | 
			
		||||
msgid "monocle"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 
 | 
			
		||||
@@ -1,49 +1,131 @@
 | 
			
		||||
# 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 <EMAIL@ADDRESS>, 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: 2021-01-08 15:15+0100\n"
 | 
			
		||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
			
		||||
"Last-Translator: ifugao\n"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\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 =="
 | 
			
		||||
msgid "ring_of_critical_damage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
# Suggested in Weblate: Inventorio :
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:34
 | 
			
		||||
msgid "ring_of_more_experience"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} takes {amount} damage."
 | 
			
		||||
msgstr "{name} recibe {amount} daño."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:28
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:123
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:148
 | 
			
		||||
msgid "Credits"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:32
 | 
			
		||||
msgid "Developers:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:38
 | 
			
		||||
msgid "Translators:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:168
 | 
			
		||||
msgid "INVENTORY"
 | 
			
		||||
msgstr "INVENTORIO"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:214
 | 
			
		||||
msgid "STALL"
 | 
			
		||||
msgstr "PUESTO"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:44
 | 
			
		||||
msgid "Inventory:"
 | 
			
		||||
msgstr "Inventorio :"
 | 
			
		||||
 | 
			
		||||
# Suggested in Weblate: ERES MUERTO
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:50
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:61
 | 
			
		||||
msgid "Equipped main:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:65
 | 
			
		||||
msgid "Equipped secondary:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:70
 | 
			
		||||
msgid "Equipped chestplate:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:74
 | 
			
		||||
msgid "Equipped helmet:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:81
 | 
			
		||||
msgid "YOU ARE DEAD"
 | 
			
		||||
msgstr "ERES MUERTO"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:85
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "Use {key} to use the ladder"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:94
 | 
			
		||||
msgid "Move to the friendly entity to talk to it"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:96
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "Use {key} then move to talk to the entity"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:33
 | 
			
		||||
msgid "I don't sell any squirrel"
 | 
			
		||||
msgstr "No vendo ninguna ardilla"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:55
 | 
			
		||||
msgid "Flower power!!"
 | 
			
		||||
msgstr "Poder de las flores!!"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:55
 | 
			
		||||
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:178
 | 
			
		||||
msgid "Bomb is exploding."
 | 
			
		||||
msgstr "La bomba está explotando."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/items.py:172
 | 
			
		||||
#: squirrelbattle/entities/items.py:365
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{player} exchanged its body with {entity}."
 | 
			
		||||
msgstr "{player} intercambió su cuerpo con {entity}."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:177
 | 
			
		||||
#: squirrelbattle/game.py:200
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "The player climbs down to the floor {floor}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:213
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "The player climbs up the floor {floor}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:304 squirrelbattle/tests/game_test.py:603
 | 
			
		||||
msgid "The buyer does not have enough money"
 | 
			
		||||
msgstr "El comprador no tiene suficiente dinero"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:349
 | 
			
		||||
msgid ""
 | 
			
		||||
"Some keys are missing in your save file.\n"
 | 
			
		||||
"Your save seems to be corrupt. It got deleted."
 | 
			
		||||
@@ -51,7 +133,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:357
 | 
			
		||||
msgid ""
 | 
			
		||||
"No player was found on this map!\n"
 | 
			
		||||
"Maybe you died?"
 | 
			
		||||
@@ -59,7 +141,7 @@ msgstr ""
 | 
			
		||||
"No jugador encontrado sobre la carta !\n"
 | 
			
		||||
"¿ Quizas murió ?"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:205
 | 
			
		||||
#: squirrelbattle/game.py:379
 | 
			
		||||
msgid ""
 | 
			
		||||
"The JSON file is not correct.\n"
 | 
			
		||||
"Your save seems corrupted. It got deleted."
 | 
			
		||||
@@ -67,28 +149,36 @@ msgstr ""
 | 
			
		||||
"El JSON archivo no es correcto.\n"
 | 
			
		||||
"Su guarda parece corrupta. Fue eliminada."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:400
 | 
			
		||||
#: squirrelbattle/interfaces.py:718
 | 
			
		||||
msgid "It's a critical hit!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:719
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} hits {opponent}."
 | 
			
		||||
msgstr "{name} golpea a {opponent}."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:412
 | 
			
		||||
#: squirrelbattle/interfaces.py:733
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} takes {amount} damage."
 | 
			
		||||
msgstr "{name} recibe {amount} daño."
 | 
			
		||||
msgid "{name} takes {damage} damage."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:414
 | 
			
		||||
#: squirrelbattle/interfaces.py:735
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} dies."
 | 
			
		||||
msgstr "{name} se muere."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/menus.py:72
 | 
			
		||||
#: squirrelbattle/interfaces.py:769
 | 
			
		||||
#, 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:368 squirrelbattle/tests/game_test.py:371
 | 
			
		||||
#: squirrelbattle/tests/game_test.py:374 squirrelbattle/tests/game_test.py:377
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:16
 | 
			
		||||
msgid "New game"
 | 
			
		||||
msgstr "Nuevo partido"
 | 
			
		||||
@@ -166,41 +256,97 @@ 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 "Key used to use ladders"
 | 
			
		||||
msgstr "Tecla para el uso de las escaleras"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:58
 | 
			
		||||
msgid "Texture pack"
 | 
			
		||||
msgstr "Paquete de texturas"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:54
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:59
 | 
			
		||||
msgid "Language"
 | 
			
		||||
msgstr "Languaje"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:57
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:62
 | 
			
		||||
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:64
 | 
			
		||||
msgid "hedgehog"
 | 
			
		||||
msgstr "erizo"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:61
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:65
 | 
			
		||||
msgid "merchant"
 | 
			
		||||
msgstr "comerciante"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:66
 | 
			
		||||
msgid "rabbit"
 | 
			
		||||
msgstr "conejo"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:62
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:67
 | 
			
		||||
msgid "sunflower"
 | 
			
		||||
msgstr "girasol"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:68
 | 
			
		||||
msgid "teddy bear"
 | 
			
		||||
msgstr "osito de peluche"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:64
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:69
 | 
			
		||||
msgid "tiger"
 | 
			
		||||
msgstr "tigre"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:70
 | 
			
		||||
msgid "eagle"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:72
 | 
			
		||||
msgid "body snatch potion"
 | 
			
		||||
msgstr "poción de intercambio"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:65
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:73
 | 
			
		||||
msgid "bomb"
 | 
			
		||||
msgstr "bomba"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:66
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:74
 | 
			
		||||
msgid "explosion"
 | 
			
		||||
msgstr "explosión"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:75
 | 
			
		||||
msgid "heart"
 | 
			
		||||
msgstr "corazón"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:76
 | 
			
		||||
msgid "sword"
 | 
			
		||||
msgstr "espada"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:77
 | 
			
		||||
msgid "helmet"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:78
 | 
			
		||||
msgid "chestplate"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:79
 | 
			
		||||
msgid "shield"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:80
 | 
			
		||||
msgid "ring of critical damage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:82
 | 
			
		||||
msgid "ring of more experience"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:84
 | 
			
		||||
msgid "monocle"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 
 | 
			
		||||
@@ -1,67 +1,126 @@
 | 
			
		||||
# 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 <EMAIL@ADDRESS>, 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-11 18:06+0100\n"
 | 
			
		||||
"POT-Creation-Date: 2021-01-08 15:15+0100\n"
 | 
			
		||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\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:113
 | 
			
		||||
msgid "== INVENTORY =="
 | 
			
		||||
msgstr "== INVENTAIRE =="
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} takes {amount} damage."
 | 
			
		||||
msgstr "{name} prend {amount} points de dégât."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:134
 | 
			
		||||
msgid "== STALL =="
 | 
			
		||||
msgstr "== STAND =="
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:28
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:123
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:148
 | 
			
		||||
msgid "Credits"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:34
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:32
 | 
			
		||||
msgid "Developers:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/creditsdisplay.py:38
 | 
			
		||||
msgid "Translators:"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:168
 | 
			
		||||
msgid "INVENTORY"
 | 
			
		||||
msgstr "INVENTAIRE"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/menudisplay.py:214
 | 
			
		||||
msgid "STALL"
 | 
			
		||||
msgstr "STAND"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:44
 | 
			
		||||
msgid "Inventory:"
 | 
			
		||||
msgstr "Inventaire :"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:53
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:61
 | 
			
		||||
msgid "Equipped main:"
 | 
			
		||||
msgstr "Équipement principal :"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:65
 | 
			
		||||
msgid "Equipped secondary:"
 | 
			
		||||
msgstr "Équipement secondaire :"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:70
 | 
			
		||||
msgid "Equipped chestplate:"
 | 
			
		||||
msgstr "Plastron équipé :"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:74
 | 
			
		||||
msgid "Equipped helmet:"
 | 
			
		||||
msgstr "Casque équipé :"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:81
 | 
			
		||||
msgid "YOU ARE DEAD"
 | 
			
		||||
msgstr "VOUS ÊTES MORT"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:85
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "Use {key} to use the ladder"
 | 
			
		||||
msgstr "Appuyez sur {key} pour utiliser l'échelle"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:94
 | 
			
		||||
msgid "Move to the friendly entity to talk to it"
 | 
			
		||||
msgstr "Avancez vers l'entité pour lui parler"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/display/statsdisplay.py:96
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "Use {key} then move to talk to the entity"
 | 
			
		||||
msgstr "Appuyez sur {key} puis déplacez-vous pour parler"
 | 
			
		||||
 | 
			
		||||
#. TODO
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:33
 | 
			
		||||
msgid "I don't sell any squirrel"
 | 
			
		||||
msgstr "Je ne vends pas d'écureuil"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:46
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:55
 | 
			
		||||
msgid "Flower power!!"
 | 
			
		||||
msgstr "Pouvoir des fleurs !"
 | 
			
		||||
msgstr "Pouvoir des fleurs !!"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:46
 | 
			
		||||
#: squirrelbattle/entities/friendly.py:55
 | 
			
		||||
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:151
 | 
			
		||||
#: squirrelbattle/entities/items.py:178
 | 
			
		||||
msgid "Bomb is exploding."
 | 
			
		||||
msgstr "La bombe explose."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/entities/items.py:224
 | 
			
		||||
#: squirrelbattle/entities/items.py:365
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{player} exchanged its body with {entity}."
 | 
			
		||||
msgstr "{player} a échangé son corps avec {entity}."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:199 squirrelbattle/tests/game_test.py:537
 | 
			
		||||
msgid "You do not have enough money"
 | 
			
		||||
msgstr ""
 | 
			
		||||
#: squirrelbattle/game.py:200
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "The player climbs down to the floor {floor}."
 | 
			
		||||
msgstr "Le joueur descend à l'étage {floor}."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:243
 | 
			
		||||
#: squirrelbattle/game.py:213
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "The player climbs up the floor {floor}."
 | 
			
		||||
msgstr "Le joueur monte à l'étage {floor}."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:304 squirrelbattle/tests/game_test.py:603
 | 
			
		||||
msgid "The buyer does not have enough money"
 | 
			
		||||
msgstr "L'acheteur n'a pas assez d'argent"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:349
 | 
			
		||||
msgid ""
 | 
			
		||||
"Some keys are missing in your save file.\n"
 | 
			
		||||
"Your save seems to be corrupt. It got deleted."
 | 
			
		||||
@@ -69,7 +128,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:251
 | 
			
		||||
#: squirrelbattle/game.py:357
 | 
			
		||||
msgid ""
 | 
			
		||||
"No player was found on this map!\n"
 | 
			
		||||
"Maybe you died?"
 | 
			
		||||
@@ -77,7 +136,7 @@ msgstr ""
 | 
			
		||||
"Aucun joueur n'a été trouvé sur la carte !\n"
 | 
			
		||||
"Peut-être êtes-vous mort ?"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/game.py:271
 | 
			
		||||
#: squirrelbattle/game.py:379
 | 
			
		||||
msgid ""
 | 
			
		||||
"The JSON file is not correct.\n"
 | 
			
		||||
"Your save seems corrupted. It got deleted."
 | 
			
		||||
@@ -85,27 +144,36 @@ msgstr ""
 | 
			
		||||
"Le fichier JSON de sauvegarde est incorrect.\n"
 | 
			
		||||
"Votre sauvegarde semble corrompue. Elle a été supprimée."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:429
 | 
			
		||||
#: squirrelbattle/interfaces.py:718
 | 
			
		||||
msgid "It's a critical hit!"
 | 
			
		||||
msgstr "C'est un coup critique !"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:719
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} hits {opponent}."
 | 
			
		||||
msgstr "{name} frappe {opponent}."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:441
 | 
			
		||||
#: squirrelbattle/interfaces.py:733
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} takes {amount} damage."
 | 
			
		||||
msgstr "{name} prend {amount} points de dégât."
 | 
			
		||||
msgid "{name} takes {damage} damage."
 | 
			
		||||
msgstr "{name} prend {damage} dégâts."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:443
 | 
			
		||||
#: squirrelbattle/interfaces.py:735
 | 
			
		||||
#, python-brace-format
 | 
			
		||||
msgid "{name} dies."
 | 
			
		||||
msgstr "{name} meurt."
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/interfaces.py:769
 | 
			
		||||
#, 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:314 squirrelbattle/tests/game_test.py:317
 | 
			
		||||
#: squirrelbattle/tests/game_test.py:320
 | 
			
		||||
#: squirrelbattle/tests/game_test.py:368 squirrelbattle/tests/game_test.py:371
 | 
			
		||||
#: squirrelbattle/tests/game_test.py:374 squirrelbattle/tests/game_test.py:377
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:16
 | 
			
		||||
msgid "New game"
 | 
			
		||||
msgstr "Nouvelle partie"
 | 
			
		||||
@@ -187,45 +255,93 @@ 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 "Key used to use ladders"
 | 
			
		||||
msgstr "Touche pour utiliser les échelles"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:58
 | 
			
		||||
msgid "Texture pack"
 | 
			
		||||
msgstr "Pack de textures"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:56
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:59
 | 
			
		||||
msgid "Language"
 | 
			
		||||
msgstr "Langue"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:59
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:62
 | 
			
		||||
msgid "player"
 | 
			
		||||
msgstr "joueur"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:61
 | 
			
		||||
msgid "tiger"
 | 
			
		||||
msgstr "tigre"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:62
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:64
 | 
			
		||||
msgid "hedgehog"
 | 
			
		||||
msgstr "hérisson"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:63
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:65
 | 
			
		||||
msgid "merchant"
 | 
			
		||||
msgstr "marchand"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:66
 | 
			
		||||
msgid "rabbit"
 | 
			
		||||
msgstr "lapin"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:64
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:67
 | 
			
		||||
msgid "sunflower"
 | 
			
		||||
msgstr "tournesol"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:68
 | 
			
		||||
msgid "teddy bear"
 | 
			
		||||
msgstr "nounours"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:66
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:69
 | 
			
		||||
msgid "tiger"
 | 
			
		||||
msgstr "tigre"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:70
 | 
			
		||||
msgid "eagle"
 | 
			
		||||
msgstr "pygargue"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:72
 | 
			
		||||
msgid "body snatch potion"
 | 
			
		||||
msgstr "potion d'arrachage de corps"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:67
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:73
 | 
			
		||||
msgid "bomb"
 | 
			
		||||
msgstr "bombe"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:68
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:74
 | 
			
		||||
msgid "explosion"
 | 
			
		||||
msgstr "explosion"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:75
 | 
			
		||||
msgid "heart"
 | 
			
		||||
msgstr "cœur"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:69
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:76
 | 
			
		||||
msgid "sword"
 | 
			
		||||
msgstr "épée"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:77
 | 
			
		||||
msgid "helmet"
 | 
			
		||||
msgstr "casque"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:78
 | 
			
		||||
msgid "chestplate"
 | 
			
		||||
msgstr "plastron"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:79
 | 
			
		||||
msgid "shield"
 | 
			
		||||
msgstr "bouclier"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:80
 | 
			
		||||
msgid "ring of critical damage"
 | 
			
		||||
msgstr "anneau de coup critique"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:82
 | 
			
		||||
msgid "ring of more experience"
 | 
			
		||||
msgstr "anneau de plus d'expérience"
 | 
			
		||||
 | 
			
		||||
#: squirrelbattle/tests/translations_test.py:84
 | 
			
		||||
msgid "monocle"
 | 
			
		||||
msgstr "monocle"
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ from .translations import gettext as _, Translator
 | 
			
		||||
 | 
			
		||||
class Menu:
 | 
			
		||||
    """
 | 
			
		||||
    A Menu object is the logical representation of a menu in the game
 | 
			
		||||
    A Menu object is the logical representation of a menu in the game.
 | 
			
		||||
    """
 | 
			
		||||
    values: list
 | 
			
		||||
 | 
			
		||||
@@ -23,26 +23,26 @@ class Menu:
 | 
			
		||||
 | 
			
		||||
    def go_up(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Moves the pointer of the menu on the previous value
 | 
			
		||||
        Moves the pointer of the menu on the previous value.
 | 
			
		||||
        """
 | 
			
		||||
        self.position = max(0, self.position - 1)
 | 
			
		||||
 | 
			
		||||
    def go_down(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Moves the pointer of the menu on the next value
 | 
			
		||||
        Moves the pointer of the menu on the next value.
 | 
			
		||||
        """
 | 
			
		||||
        self.position = min(len(self.values) - 1, self.position + 1)
 | 
			
		||||
 | 
			
		||||
    def validate(self) -> Any:
 | 
			
		||||
        """
 | 
			
		||||
        Selects the value that is pointed by the menu pointer
 | 
			
		||||
        Selects the value that is pointed by the menu pointer.
 | 
			
		||||
        """
 | 
			
		||||
        return self.values[self.position]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MainMenuValues(Enum):
 | 
			
		||||
    """
 | 
			
		||||
    Values of the main menu
 | 
			
		||||
    Values of the main menu.
 | 
			
		||||
    """
 | 
			
		||||
    START = "New game"
 | 
			
		||||
    RESUME = "Resume"
 | 
			
		||||
@@ -57,14 +57,14 @@ class MainMenuValues(Enum):
 | 
			
		||||
 | 
			
		||||
class MainMenu(Menu):
 | 
			
		||||
    """
 | 
			
		||||
    A special instance of a menu : the main menu
 | 
			
		||||
    A special instance of a menu : the main menu.
 | 
			
		||||
    """
 | 
			
		||||
    values = [e for e in MainMenuValues]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SettingsMenu(Menu):
 | 
			
		||||
    """
 | 
			
		||||
    A special instance of a menu : the settings menu
 | 
			
		||||
    A special instance of a menu : the settings menu.
 | 
			
		||||
    """
 | 
			
		||||
    waiting_for_key: bool = False
 | 
			
		||||
 | 
			
		||||
@@ -75,7 +75,7 @@ class SettingsMenu(Menu):
 | 
			
		||||
    def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
 | 
			
		||||
                           game: Any) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        In the setting menu, we van select a setting and change it
 | 
			
		||||
        In the setting menu, we can select a setting and change it.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.waiting_for_key:
 | 
			
		||||
            # Navigate normally through the menu.
 | 
			
		||||
@@ -121,22 +121,40 @@ class SettingsMenu(Menu):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InventoryMenu(Menu):
 | 
			
		||||
    """
 | 
			
		||||
    A special instance of a menu : the menu for the inventory of the player.
 | 
			
		||||
    """
 | 
			
		||||
    player: Player
 | 
			
		||||
 | 
			
		||||
    def update_player(self, player: Player) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Updates the player.
 | 
			
		||||
        """
 | 
			
		||||
        self.player = player
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def values(self) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Returns the values of the menu.
 | 
			
		||||
        """
 | 
			
		||||
        return self.player.inventory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StoreMenu(Menu):
 | 
			
		||||
    merchant: Merchant
 | 
			
		||||
    """
 | 
			
		||||
    A special instance of a menu : the menu for the inventory of a merchant.
 | 
			
		||||
    """
 | 
			
		||||
    merchant: Merchant = None
 | 
			
		||||
 | 
			
		||||
    def update_merchant(self, merchant: Merchant) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Updates the merchant.
 | 
			
		||||
        """
 | 
			
		||||
        self.merchant = merchant
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def values(self) -> list:
 | 
			
		||||
        return self.merchant.inventory
 | 
			
		||||
        """
 | 
			
		||||
        Returns the values of the menu.
 | 
			
		||||
        """
 | 
			
		||||
        return self.merchant.inventory if self.merchant else []
 | 
			
		||||
 
 | 
			
		||||
@@ -13,9 +13,10 @@ from .translations import gettext as _
 | 
			
		||||
class Settings:
 | 
			
		||||
    """
 | 
			
		||||
    This class stores the settings of the game.
 | 
			
		||||
    Settings can be get by using for example settings.TEXTURE_PACK directly.
 | 
			
		||||
    The comment can be get by using settings.get_comment('TEXTURE_PACK').
 | 
			
		||||
    We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
 | 
			
		||||
    Settings can be obtained by using for example settings.TEXTURE_PACK
 | 
			
		||||
    directly.
 | 
			
		||||
    The comment can be obtained by using settings.get_comment('TEXTURE_PACK').
 | 
			
		||||
    We can set the setting by simply using settings.TEXTURE_PACK = 'new_key'
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.KEY_UP_PRIMARY = ['z', 'Main key to move up']
 | 
			
		||||
@@ -32,6 +33,8 @@ class Settings:
 | 
			
		||||
        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.KEY_LADDER = ['<', 'Key used to use ladders']
 | 
			
		||||
        self.TEXTURE_PACK = ['ascii', 'Texture pack']
 | 
			
		||||
        self.LOCALE = [locale.getlocale()[0][:2], 'Language']
 | 
			
		||||
 | 
			
		||||
@@ -49,7 +52,7 @@ class Settings:
 | 
			
		||||
 | 
			
		||||
    def get_comment(self, item: str) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Retrieve the comment of a setting.
 | 
			
		||||
        Retrieves the comment relative to a setting.
 | 
			
		||||
        """
 | 
			
		||||
        if item in self.settings_keys:
 | 
			
		||||
            return _(object.__getattribute__(self, item)[1])
 | 
			
		||||
@@ -60,21 +63,22 @@ class Settings:
 | 
			
		||||
    @property
 | 
			
		||||
    def settings_keys(self) -> Generator[str, Any, None]:
 | 
			
		||||
        """
 | 
			
		||||
        Get the list of all parameters.
 | 
			
		||||
        Gets the list of all parameters.
 | 
			
		||||
        """
 | 
			
		||||
        return (key for key in self.__dict__)
 | 
			
		||||
 | 
			
		||||
    def loads_from_string(self, json_str: str) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Dump settings
 | 
			
		||||
        Loads settings.
 | 
			
		||||
        """
 | 
			
		||||
        d = json.loads(json_str)
 | 
			
		||||
        for key in d:
 | 
			
		||||
            setattr(self, key, d[key])
 | 
			
		||||
            if hasattr(self, key):
 | 
			
		||||
                setattr(self, key, d[key])
 | 
			
		||||
 | 
			
		||||
    def dumps_to_string(self) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Dump settings
 | 
			
		||||
        Dumps settings.
 | 
			
		||||
        """
 | 
			
		||||
        d = dict()
 | 
			
		||||
        for key in self.settings_keys:
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ from types import TracebackType
 | 
			
		||||
class TermManager:  # pragma: no cover
 | 
			
		||||
    """
 | 
			
		||||
    The TermManager object initializes the terminal, returns a screen object and
 | 
			
		||||
    de-initializes the terminal after use
 | 
			
		||||
    de-initializes the terminal after use.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.screen = curses.initscr()
 | 
			
		||||
@@ -21,7 +21,7 @@ class TermManager:  # pragma: no cover
 | 
			
		||||
        # make cursor invisible
 | 
			
		||||
        curses.curs_set(False)
 | 
			
		||||
        # Catch mouse events
 | 
			
		||||
        curses.mousemask(True)
 | 
			
		||||
        curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
 | 
			
		||||
        # Enable colors
 | 
			
		||||
        curses.start_color()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,13 @@
 | 
			
		||||
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
import random
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item
 | 
			
		||||
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear
 | 
			
		||||
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item, \
 | 
			
		||||
    Explosion
 | 
			
		||||
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit,\
 | 
			
		||||
    TeddyBear, GiantSeaEagle
 | 
			
		||||
from squirrelbattle.entities.friendly import Trumpet
 | 
			
		||||
from squirrelbattle.entities.player import Player
 | 
			
		||||
from squirrelbattle.interfaces import Entity, Map
 | 
			
		||||
from squirrelbattle.resources import ResourceManager
 | 
			
		||||
@@ -13,16 +16,17 @@ from squirrelbattle.resources import ResourceManager
 | 
			
		||||
class TestEntities(unittest.TestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Load example map that can be used in tests.
 | 
			
		||||
        Loads example map that can be used in tests.
 | 
			
		||||
        """
 | 
			
		||||
        self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
 | 
			
		||||
        self.player = Player()
 | 
			
		||||
        self.player.constitution = 0
 | 
			
		||||
        self.map.add_entity(self.player)
 | 
			
		||||
        self.player.move(self.map.start_y, self.map.start_x)
 | 
			
		||||
 | 
			
		||||
    def test_basic_entities(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test some random stuff with basic entities.
 | 
			
		||||
        Tests some random stuff with basic entities.
 | 
			
		||||
        """
 | 
			
		||||
        entity = Entity()
 | 
			
		||||
        entity.move(42, 64)
 | 
			
		||||
@@ -37,7 +41,7 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_fighting_entities(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test some random stuff with fighting entities.
 | 
			
		||||
        Tests some random stuff with fighting entities.
 | 
			
		||||
        """
 | 
			
		||||
        entity = Tiger()
 | 
			
		||||
        self.map.add_entity(entity)
 | 
			
		||||
@@ -53,20 +57,21 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
        self.assertTrue(entity.dead)
 | 
			
		||||
 | 
			
		||||
        entity = Rabbit()
 | 
			
		||||
        entity.critical = 0
 | 
			
		||||
        self.map.add_entity(entity)
 | 
			
		||||
        entity.move(15, 44)
 | 
			
		||||
        # Move randomly
 | 
			
		||||
        self.map.tick()
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertFalse(entity.y == 15 and entity.x == 44)
 | 
			
		||||
 | 
			
		||||
        # Move to the player
 | 
			
		||||
        entity.move(3, 6)
 | 
			
		||||
        self.map.tick()
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertTrue(entity.y == 2 and entity.x == 6)
 | 
			
		||||
 | 
			
		||||
        # Rabbit should fight
 | 
			
		||||
        old_health = self.player.health
 | 
			
		||||
        self.map.tick()
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertTrue(entity.y == 2 and entity.x == 6)
 | 
			
		||||
        self.assertEqual(old_health - entity.strength, self.player.health)
 | 
			
		||||
        self.assertEqual(self.map.logs.messages[-1],
 | 
			
		||||
@@ -74,6 +79,7 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
{self.player.name.capitalize()} takes {entity.strength} damage.")
 | 
			
		||||
 | 
			
		||||
        # Fight the rabbit
 | 
			
		||||
        self.player.critical = 0
 | 
			
		||||
        old_health = entity.health
 | 
			
		||||
        self.player.move_down()
 | 
			
		||||
        self.assertEqual(entity.health, old_health - self.player.strength)
 | 
			
		||||
@@ -88,9 +94,50 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
        self.assertTrue(entity.dead)
 | 
			
		||||
        self.assertGreaterEqual(self.player.current_xp, 3)
 | 
			
		||||
 | 
			
		||||
        # Test the familiars
 | 
			
		||||
        fam = Trumpet()
 | 
			
		||||
        entity = Rabbit()
 | 
			
		||||
        self.map.add_entity(entity)
 | 
			
		||||
        self.map.add_entity(fam)
 | 
			
		||||
        self.player.move(1, 6)
 | 
			
		||||
        entity.move(2, 6)
 | 
			
		||||
        fam.move(2, 7)
 | 
			
		||||
 | 
			
		||||
        # Test fighting
 | 
			
		||||
        entity.health = 2
 | 
			
		||||
        entity.paths = []
 | 
			
		||||
        entity.recalculate_paths()
 | 
			
		||||
        fam.target = entity
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertTrue(entity.dead)
 | 
			
		||||
 | 
			
		||||
        # Test finding a new target
 | 
			
		||||
        entity2 = Rabbit()
 | 
			
		||||
        self.map.add_entity(entity2)
 | 
			
		||||
        entity2.move(2, 6)
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertTrue(fam.target == entity2)
 | 
			
		||||
        self.map.remove_entity(entity2)
 | 
			
		||||
 | 
			
		||||
        # Test following the player and finding the player as target
 | 
			
		||||
        self.player.move(5, 5)
 | 
			
		||||
        fam.move(4, 5)
 | 
			
		||||
        fam.target = None
 | 
			
		||||
        self.player.move_down()
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertTrue(fam.target == self.player)
 | 
			
		||||
        self.assertEqual(fam.y, 5)
 | 
			
		||||
        self.assertEqual(fam.x, 5)
 | 
			
		||||
 | 
			
		||||
        # Test random move
 | 
			
		||||
        fam.move(13, 20)
 | 
			
		||||
        fam.target = self.player
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertTrue(fam.x != 20 or fam.y != 13)
 | 
			
		||||
 | 
			
		||||
    def test_items(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test some random stuff with items.
 | 
			
		||||
        Tests some random stuff with items.
 | 
			
		||||
        """
 | 
			
		||||
        item = Item()
 | 
			
		||||
        self.map.add_entity(item)
 | 
			
		||||
@@ -111,7 +158,7 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_bombs(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test some random stuff with bombs.
 | 
			
		||||
        Tests some random stuff with bombs.
 | 
			
		||||
        """
 | 
			
		||||
        item = Bomb()
 | 
			
		||||
        hedgehog = Hedgehog()
 | 
			
		||||
@@ -132,16 +179,30 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
        self.assertEqual(item.y, 42)
 | 
			
		||||
        self.assertEqual(item.x, 42)
 | 
			
		||||
        # Wait for the explosion
 | 
			
		||||
        for ignored in range(5):
 | 
			
		||||
        for _ignored in range(5):
 | 
			
		||||
            item.act(self.map)
 | 
			
		||||
        self.assertTrue(hedgehog.dead)
 | 
			
		||||
        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:
 | 
			
		||||
        """
 | 
			
		||||
        Test some random stuff with hearts.
 | 
			
		||||
        Tests some random stuff with hearts.
 | 
			
		||||
        """
 | 
			
		||||
        item = Heart()
 | 
			
		||||
        self.map.add_entity(item)
 | 
			
		||||
@@ -156,7 +217,7 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_body_snatch_potion(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test some random stuff with body snatch potions.
 | 
			
		||||
        Tests some random stuff with body snatch potions.
 | 
			
		||||
        """
 | 
			
		||||
        item = BodySnatchPotion()
 | 
			
		||||
        self.map.add_entity(item)
 | 
			
		||||
@@ -174,7 +235,7 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_players(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test some random stuff with players.
 | 
			
		||||
        Tests some random stuff with players.
 | 
			
		||||
        """
 | 
			
		||||
        player = Player()
 | 
			
		||||
        self.map.add_entity(player)
 | 
			
		||||
@@ -204,3 +265,17 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        player_state = player.save_state()
 | 
			
		||||
        self.assertEqual(player_state["current_xp"], 10)
 | 
			
		||||
 | 
			
		||||
    def test_critical_hit(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that critical hits are working.
 | 
			
		||||
        """
 | 
			
		||||
        random.seed(2)  # Next random.randint(1, 100) will output 8
 | 
			
		||||
        self.player.critical = 10
 | 
			
		||||
        sea_eagle = GiantSeaEagle()
 | 
			
		||||
        self.map.add_entity(sea_eagle)
 | 
			
		||||
        sea_eagle.move(2, 6)
 | 
			
		||||
        old_health = sea_eagle.health
 | 
			
		||||
        self.player.hit(sea_eagle)
 | 
			
		||||
        self.assertEqual(sea_eagle.health,
 | 
			
		||||
                         old_health - self.player.strength * 4)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
import curses
 | 
			
		||||
import os
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
@@ -8,11 +9,13 @@ from ..bootstrap import Bootstrap
 | 
			
		||||
from ..display.display import Display
 | 
			
		||||
from ..display.display_manager import DisplayManager
 | 
			
		||||
from ..entities.friendly import Merchant, Sunflower
 | 
			
		||||
from ..entities.items import Bomb, Heart, Sword
 | 
			
		||||
from ..entities.items import Bomb, Heart, Sword, Explosion, Shield, Helmet, \
 | 
			
		||||
    Chestplate, RingCritical, Monocle
 | 
			
		||||
from ..entities.monsters import GiantSeaEagle
 | 
			
		||||
from ..entities.player import Player
 | 
			
		||||
from ..enums import DisplayActions
 | 
			
		||||
from ..game import Game, KeyValues, GameMode
 | 
			
		||||
from ..interfaces import Tile, Map
 | 
			
		||||
from ..interfaces import Map, Tile
 | 
			
		||||
from ..menus import MainMenuValues
 | 
			
		||||
from ..resources import ResourceManager
 | 
			
		||||
from ..settings import Settings
 | 
			
		||||
@@ -22,21 +25,21 @@ from ..translations import gettext as _, Translator
 | 
			
		||||
class TestGame(unittest.TestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Setup game.
 | 
			
		||||
        Sets the game up.
 | 
			
		||||
        """
 | 
			
		||||
        self.game = Game()
 | 
			
		||||
        self.game.new_game()
 | 
			
		||||
        self.game.map = Map.load(ResourceManager.get_asset_path(
 | 
			
		||||
            "example_map.txt"))
 | 
			
		||||
        self.game.player.move(self.game.map.start_y, self.game.map.start_x)
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
    def test_load_game(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Save a game and reload it.
 | 
			
		||||
        Saves a game and reloads it.
 | 
			
		||||
        """
 | 
			
		||||
        bomb = Bomb()
 | 
			
		||||
        self.game.map.add_entity(bomb)
 | 
			
		||||
@@ -63,6 +66,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        new_state = self.game.save_state()
 | 
			
		||||
        self.assertEqual(old_state, new_state)
 | 
			
		||||
        self.assertIsNone(self.game.message)
 | 
			
		||||
 | 
			
		||||
        # Ensure that the bomb is loaded
 | 
			
		||||
        self.assertTrue(self.game.player.inventory)
 | 
			
		||||
@@ -90,7 +94,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_bootstrap_fail(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that the test can't play the game,
 | 
			
		||||
        Ensures that the test can't play the game,
 | 
			
		||||
        because there is no associated shell.
 | 
			
		||||
        Yeah, that's only for coverage.
 | 
			
		||||
        """
 | 
			
		||||
@@ -99,7 +103,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_key_translation(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test key bindings.
 | 
			
		||||
        Tests key bindings.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.settings = Settings()
 | 
			
		||||
 | 
			
		||||
@@ -145,6 +149,12 @@ 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.KEY_LADDER, self.game.settings),
 | 
			
		||||
            KeyValues.LADDER)
 | 
			
		||||
        self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
 | 
			
		||||
                         KeyValues.SPACE)
 | 
			
		||||
        self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
 | 
			
		||||
@@ -152,7 +162,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_key_press(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Press a key and see what is done.
 | 
			
		||||
        Presses a key and asserts what is done is correct.
 | 
			
		||||
        """
 | 
			
		||||
        self.assertEqual(self.game.state, GameMode.MAINMENU)
 | 
			
		||||
        self.assertEqual(self.game.main_menu.validate(),
 | 
			
		||||
@@ -238,17 +248,28 @@ 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.
 | 
			
		||||
        Simulates mouse clicks.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.MAINMENU
 | 
			
		||||
 | 
			
		||||
        # Change the color of the artwork
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 0, 10,
 | 
			
		||||
                                  curses.BUTTON1_CLICKED)
 | 
			
		||||
 | 
			
		||||
        # Settings menu
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 25, 21)
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 25, 21,
 | 
			
		||||
                                  curses.BUTTON1_CLICKED)
 | 
			
		||||
        self.assertEqual(self.game.main_menu.position, 4)
 | 
			
		||||
        self.assertEqual(self.game.state, GameMode.SETTINGS)
 | 
			
		||||
 | 
			
		||||
@@ -260,17 +281,19 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
        self.game.state = GameMode.INVENTORY
 | 
			
		||||
 | 
			
		||||
        # Click nowhere
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 0, 0)
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 0, 0,
 | 
			
		||||
                                  curses.BUTTON1_CLICKED)
 | 
			
		||||
        self.assertEqual(self.game.state, GameMode.INVENTORY)
 | 
			
		||||
 | 
			
		||||
        # Click on the second item
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 8, 25)
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 8, 25,
 | 
			
		||||
                                  curses.BUTTON1_CLICKED)
 | 
			
		||||
        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.
 | 
			
		||||
        Ensures that the start button starts a new game.
 | 
			
		||||
        """
 | 
			
		||||
        old_map = self.game.map
 | 
			
		||||
        old_player = self.game.player
 | 
			
		||||
@@ -293,7 +316,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_settings_menu(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that the settings menu is working properly.
 | 
			
		||||
        Ensures that the settings menu is working properly.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.settings = Settings()
 | 
			
		||||
 | 
			
		||||
@@ -305,13 +328,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)
 | 
			
		||||
@@ -332,7 +355,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
        self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
 | 
			
		||||
 | 
			
		||||
        # Navigate to "texture pack"
 | 
			
		||||
        for ignored in range(10):
 | 
			
		||||
        for ignored in range(12):
 | 
			
		||||
            self.game.handle_key_pressed(KeyValues.DOWN)
 | 
			
		||||
 | 
			
		||||
        # Change texture pack
 | 
			
		||||
@@ -379,7 +402,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_dead_screen(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Kill player and render dead screen.
 | 
			
		||||
        Kills the player and renders the dead message on the fake screen.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
        # Kill player
 | 
			
		||||
@@ -395,13 +418,14 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_not_implemented(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Check that some functions are not implemented, only for coverage.
 | 
			
		||||
        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:
 | 
			
		||||
        """
 | 
			
		||||
        Display error messages.
 | 
			
		||||
        Displays error messages.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.message = "I am an error"
 | 
			
		||||
        self.game.display_actions(DisplayActions.UPDATE)
 | 
			
		||||
@@ -411,7 +435,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_inventory_menu(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Open the inventory menu and interact with items.
 | 
			
		||||
        Opens the inventory menu and interacts with items.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
        # Open and close the inventory
 | 
			
		||||
@@ -472,7 +496,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_talk_to_sunflowers(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Interact with sunflowers
 | 
			
		||||
        Interacts with sunflowers.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
 | 
			
		||||
@@ -504,8 +528,8 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
        self.assertEqual(self.game.state, GameMode.PLAY)
 | 
			
		||||
        self.assertTrue(self.game.logs.messages)
 | 
			
		||||
        # Ensure that the message is a good message
 | 
			
		||||
        self.assertIn(self.game.logs.messages[1][21:],
 | 
			
		||||
                      Sunflower.dialogue_option)
 | 
			
		||||
        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(sunflower.y + 1, sunflower.x)
 | 
			
		||||
@@ -523,7 +547,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_talk_to_merchant(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Interact with merchants
 | 
			
		||||
        Interacts with merchants.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
 | 
			
		||||
@@ -545,21 +569,28 @@ 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, 8, 25)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.ENTER)
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 7, 25,
 | 
			
		||||
                                  curses.BUTTON1_CLICKED)
 | 
			
		||||
        self.assertIn(item, self.game.player.inventory)
 | 
			
		||||
        self.assertNotIn(item, merchant.inventory)
 | 
			
		||||
 | 
			
		||||
        # Buy a heart
 | 
			
		||||
        merchant.inventory[1] = Heart()
 | 
			
		||||
        self.game.display_actions(DisplayActions.REFRESH)
 | 
			
		||||
        item = self.game.store_menu.validate()
 | 
			
		||||
        self.assertIn(item, merchant.inventory)
 | 
			
		||||
        self.assertEqual(item, merchant.inventory[1])
 | 
			
		||||
@@ -576,9 +607,169 @@ 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)
 | 
			
		||||
 | 
			
		||||
    def test_equipment(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that equipment is working.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.INVENTORY
 | 
			
		||||
 | 
			
		||||
        # sword goes into the main equipment slot
 | 
			
		||||
        sword = Sword()
 | 
			
		||||
        sword.hold(self.game.player)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.equipped_main, sword)
 | 
			
		||||
        self.assertFalse(self.game.player.inventory)
 | 
			
		||||
 | 
			
		||||
        # shield goes into the secondary equipment slot
 | 
			
		||||
        shield = Shield()
 | 
			
		||||
        shield.hold(self.game.player)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.equipped_secondary, shield)
 | 
			
		||||
        self.assertFalse(self.game.player.inventory)
 | 
			
		||||
 | 
			
		||||
        # helmet goes into the helmet slot
 | 
			
		||||
        helmet = Helmet()
 | 
			
		||||
        helmet.hold(self.game.player)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.equipped_helmet, helmet)
 | 
			
		||||
        self.assertFalse(self.game.player.inventory)
 | 
			
		||||
 | 
			
		||||
        # helmet goes into the armor slot
 | 
			
		||||
        chestplate = Chestplate()
 | 
			
		||||
        chestplate.hold(self.game.player)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.equipped_armor, chestplate)
 | 
			
		||||
        self.assertFalse(self.game.player.inventory)
 | 
			
		||||
 | 
			
		||||
        # Use bomb
 | 
			
		||||
        bomb = Bomb()
 | 
			
		||||
        bomb.hold(self.game.player)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.equipped_secondary, bomb)
 | 
			
		||||
        self.assertIn(shield, self.game.player.inventory)
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.USE)
 | 
			
		||||
        self.assertIsNone(self.game.player.equipped_secondary)
 | 
			
		||||
        self.game.state = GameMode.INVENTORY
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.equipped_secondary, shield)
 | 
			
		||||
        self.assertFalse(self.game.player.inventory)
 | 
			
		||||
 | 
			
		||||
        # Reequip, which is useless but covers code
 | 
			
		||||
        sword.equip()
 | 
			
		||||
        shield.equip()
 | 
			
		||||
        helmet.equip()
 | 
			
		||||
        chestplate.equip()
 | 
			
		||||
        self.game.save_state()
 | 
			
		||||
 | 
			
		||||
        # Unequip all
 | 
			
		||||
        sword.unequip()
 | 
			
		||||
        shield.unequip()
 | 
			
		||||
        helmet.unequip()
 | 
			
		||||
        chestplate.unequip()
 | 
			
		||||
        self.assertIsNone(self.game.player.equipped_main)
 | 
			
		||||
        self.assertIsNone(self.game.player.equipped_secondary)
 | 
			
		||||
        self.assertIsNone(self.game.player.equipped_helmet)
 | 
			
		||||
        self.assertIsNone(self.game.player.equipped_armor)
 | 
			
		||||
        self.assertIn(sword, self.game.player.inventory)
 | 
			
		||||
        self.assertIn(shield, self.game.player.inventory)
 | 
			
		||||
        self.assertIn(helmet, self.game.player.inventory)
 | 
			
		||||
        self.assertIn(chestplate, self.game.player.inventory)
 | 
			
		||||
 | 
			
		||||
        # Test rings
 | 
			
		||||
        self.game.player.inventory.clear()
 | 
			
		||||
        ring = RingCritical()
 | 
			
		||||
        ring.hold(self.game.player)
 | 
			
		||||
        old_critical = self.game.player.critical
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.critical,
 | 
			
		||||
                         old_critical + ring.critical)
 | 
			
		||||
        self.game.save_state()
 | 
			
		||||
        ring.unequip()
 | 
			
		||||
 | 
			
		||||
    def test_monocle(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        The player is wearing a monocle, then the stats are displayed.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
 | 
			
		||||
        monocle = Monocle()
 | 
			
		||||
        monocle.hold(self.game.player)
 | 
			
		||||
        monocle.equip()
 | 
			
		||||
 | 
			
		||||
        sea_eagle = GiantSeaEagle()
 | 
			
		||||
        self.game.map.add_entity(sea_eagle)
 | 
			
		||||
        sea_eagle.move(2, 6)
 | 
			
		||||
 | 
			
		||||
        self.game.display_actions(DisplayActions.REFRESH)
 | 
			
		||||
 | 
			
		||||
    def test_ladders(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that the player can climb on ladders.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.game.player.map.floor, 0)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.LADDER)
 | 
			
		||||
        self.assertEqual(self.game.player.map.floor, 0)
 | 
			
		||||
 | 
			
		||||
        # Move nowhere
 | 
			
		||||
        self.game.player.move(10, 10)
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.LADDER)
 | 
			
		||||
        self.assertEqual(self.game.player.map.floor, 0)
 | 
			
		||||
 | 
			
		||||
        # Move down
 | 
			
		||||
        self.game.player.move(3, 40)  # Move on a ladder
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.LADDER)
 | 
			
		||||
        self.assertEqual(self.game.map_index, 1)
 | 
			
		||||
        self.assertEqual(self.game.player.map.floor, 1)
 | 
			
		||||
        self.assertEqual(self.game.player.y, 1)
 | 
			
		||||
        self.assertEqual(self.game.player.x, 17)
 | 
			
		||||
        self.game.display_actions(DisplayActions.UPDATE)
 | 
			
		||||
 | 
			
		||||
        # Move up
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.LADDER)
 | 
			
		||||
        self.assertEqual(self.game.player.map.floor, 0)
 | 
			
		||||
        self.assertEqual(self.game.player.y, 3)
 | 
			
		||||
        self.assertEqual(self.game.player.x, 40)
 | 
			
		||||
        self.game.display_actions(DisplayActions.UPDATE)
 | 
			
		||||
 | 
			
		||||
    def test_credits(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Load credits menu.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.MAINMENU
 | 
			
		||||
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 41, 41,
 | 
			
		||||
                                  curses.BUTTON1_CLICKED)
 | 
			
		||||
        self.assertEqual(self.game.state, GameMode.CREDITS)
 | 
			
		||||
        self.game.display_actions(DisplayActions.MOUSE, 21, 21,
 | 
			
		||||
                                  curses.BUTTON1_CLICKED)
 | 
			
		||||
        self.game.display_actions(DisplayActions.REFRESH)
 | 
			
		||||
 | 
			
		||||
        self.game.state = GameMode.CREDITS
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.ENTER)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.game.state, GameMode.MAINMENU)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,14 @@
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from squirrelbattle.display.texturepack import TexturePack
 | 
			
		||||
from squirrelbattle.interfaces import Map, Tile
 | 
			
		||||
from squirrelbattle.interfaces import Map, Tile, Slope
 | 
			
		||||
from squirrelbattle.resources import ResourceManager
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestInterfaces(unittest.TestCase):
 | 
			
		||||
    def test_map(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Create a map and check that it is well parsed.
 | 
			
		||||
        Creates a map and checks that it is well parsed.
 | 
			
		||||
        """
 | 
			
		||||
        m = Map.load_from_string("0 0\n.#\n#.\n")
 | 
			
		||||
        self.assertEqual(m.width, 2)
 | 
			
		||||
@@ -20,7 +20,7 @@ class TestInterfaces(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_load_map(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Try to load a map from a file.
 | 
			
		||||
        Tries to load a map from a file.
 | 
			
		||||
        """
 | 
			
		||||
        m = Map.load(ResourceManager.get_asset_path("example_map.txt"))
 | 
			
		||||
        self.assertEqual(m.width, 52)
 | 
			
		||||
@@ -28,7 +28,7 @@ class TestInterfaces(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_tiles(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test some things about tiles.
 | 
			
		||||
        Tests some things about tiles.
 | 
			
		||||
        """
 | 
			
		||||
        self.assertFalse(Tile.FLOOR.is_wall())
 | 
			
		||||
        self.assertTrue(Tile.WALL.is_wall())
 | 
			
		||||
@@ -37,3 +37,21 @@ class TestInterfaces(unittest.TestCase):
 | 
			
		||||
        self.assertFalse(Tile.WALL.can_walk())
 | 
			
		||||
        self.assertFalse(Tile.EMPTY.can_walk())
 | 
			
		||||
        self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')
 | 
			
		||||
 | 
			
		||||
    def test_slope(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Test good behaviour of slopes (basically vectors, compared according to
 | 
			
		||||
        the determinant)
 | 
			
		||||
        """
 | 
			
		||||
        a = Slope(1, 1)
 | 
			
		||||
        b = Slope(0, 1)
 | 
			
		||||
        self.assertTrue(b < a)
 | 
			
		||||
        self.assertTrue(b <= a)
 | 
			
		||||
        self.assertTrue(a <= a)
 | 
			
		||||
        self.assertTrue(a == a)
 | 
			
		||||
        self.assertTrue(a > b)
 | 
			
		||||
        self.assertTrue(a >= b)
 | 
			
		||||
 | 
			
		||||
    # def test_visibility(self) -> None:
 | 
			
		||||
        # m = Map.load(ResourceManager.get_asset_path("example_map_3.txt"))
 | 
			
		||||
        # m.compute_visibility(1, 1, 50)
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
@@ -24,3 +24,6 @@ class FakePad:
 | 
			
		||||
 | 
			
		||||
    def getmaxyx(self) -> Tuple[int, int]:
 | 
			
		||||
        return 42, 42
 | 
			
		||||
 | 
			
		||||
    def inch(self, y: int, x: int) -> str:
 | 
			
		||||
        return "i"
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class TestSettings(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_settings(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that settings are well loaded.
 | 
			
		||||
        Ensures that settings are well loaded.
 | 
			
		||||
        """
 | 
			
		||||
        settings = Settings()
 | 
			
		||||
        self.assertEqual(settings.KEY_UP_PRIMARY, 'z')
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class TestTranslations(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_main_menu_translation(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that the main menu is translated.
 | 
			
		||||
        Ensures that the main menu is translated.
 | 
			
		||||
        """
 | 
			
		||||
        self.assertEqual(_("New game"), "Nouvelle partie")
 | 
			
		||||
        self.assertEqual(_("Resume"), "Continuer")
 | 
			
		||||
@@ -22,7 +22,7 @@ class TestTranslations(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_settings_menu_translation(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that the settings menu is translated.
 | 
			
		||||
        Ensures that the settings menu is translated.
 | 
			
		||||
        """
 | 
			
		||||
        self.assertEqual(_("Main key to move up"),
 | 
			
		||||
                         "Touche principale pour aller vers le haut")
 | 
			
		||||
@@ -52,18 +52,33 @@ class TestTranslations(unittest.TestCase):
 | 
			
		||||
                         "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(_("Key used to use ladders"),
 | 
			
		||||
                         "Touche pour utiliser les échelles")
 | 
			
		||||
        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(_("eagle"), "pygargue")
 | 
			
		||||
 | 
			
		||||
        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")
 | 
			
		||||
        self.assertEqual(_("helmet"), "casque")
 | 
			
		||||
        self.assertEqual(_("chestplate"), "plastron")
 | 
			
		||||
        self.assertEqual(_("shield"), "bouclier")
 | 
			
		||||
        self.assertEqual(_("ring of critical damage"),
 | 
			
		||||
                         "anneau de coup critique")
 | 
			
		||||
        self.assertEqual(_("ring of more experience"),
 | 
			
		||||
                         "anneau de plus d'expérience")
 | 
			
		||||
        self.assertEqual(_("monocle"), "monocle")
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class Translator:
 | 
			
		||||
    """
 | 
			
		||||
    This module uses gettext to translate strings.
 | 
			
		||||
    Translator.setlocale defines the language of the strings,
 | 
			
		||||
    then gettext() translates the message.
 | 
			
		||||
    then gettext() translates the messages.
 | 
			
		||||
    """
 | 
			
		||||
    SUPPORTED_LOCALES: List[str] = ["de", "en", "es", "fr"]
 | 
			
		||||
    locale: str = "en"
 | 
			
		||||
@@ -22,7 +22,7 @@ class Translator:
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def refresh_translations(cls) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Load compiled translations.
 | 
			
		||||
        Loads compiled translations.
 | 
			
		||||
        """
 | 
			
		||||
        for language in cls.SUPPORTED_LOCALES:
 | 
			
		||||
            rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
 | 
			
		||||
@@ -37,7 +37,7 @@ class Translator:
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def setlocale(cls, lang: str) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Define the language used to translate the game.
 | 
			
		||||
        Defines the language used to translate the game.
 | 
			
		||||
        The language must be supported, otherwise nothing is done.
 | 
			
		||||
        """
 | 
			
		||||
        lang = lang[:2]
 | 
			
		||||
@@ -51,7 +51,7 @@ class Translator:
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def makemessages(cls) -> None:  # pragma: no cover
 | 
			
		||||
        """
 | 
			
		||||
        Analyse all strings in the project and extract them.
 | 
			
		||||
        Analyses all strings in the project and extracts them.
 | 
			
		||||
        """
 | 
			
		||||
        for language in cls.SUPPORTED_LOCALES:
 | 
			
		||||
            if language == "en":
 | 
			
		||||
@@ -83,7 +83,7 @@ class Translator:
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def compilemessages(cls) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Compile translation messages from source files.
 | 
			
		||||
        Compiles translation messages from source files.
 | 
			
		||||
        """
 | 
			
		||||
        for language in cls.SUPPORTED_LOCALES:
 | 
			
		||||
            if language == "en":
 | 
			
		||||
@@ -99,7 +99,7 @@ class Translator:
 | 
			
		||||
 | 
			
		||||
def gettext(message: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Translate a message.
 | 
			
		||||
    Translates a message.
 | 
			
		||||
    """
 | 
			
		||||
    return Translator.get_translator().gettext(message)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user