diff --git a/.gitignore b/.gitignore index 8499d7c..e477e04 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ env/ venv/ +local/ .coverage .pytest_cache/ diff --git a/squirrelbattle/assets/example_map.txt b/squirrelbattle/assets/example_map.txt index 5aaade9..be2e798 100644 --- a/squirrelbattle/assets/example_map.txt +++ b/squirrelbattle/assets/example_map.txt @@ -1,8 +1,8 @@ 1 6 ####### ############# - #.....# #...........# + #.H...# #...........# #.....# #####...........# - #.....# #...............# + #.....# #............H..# #.##### #.###...........# #.# #.# #...........# #.# #.# ############# diff --git a/squirrelbattle/assets/example_map_2.txt b/squirrelbattle/assets/example_map_2.txt index 8864f04..b9c751f 100644 --- a/squirrelbattle/assets/example_map_2.txt +++ b/squirrelbattle/assets/example_map_2.txt @@ -1,6 +1,6 @@ 1 17 ########### ######### - #.........# #.......# + #....H....# #.......# #.........# ############.......# #.........###############..........#.......############## #.........#........................#....................# @@ -13,7 +13,7 @@ ########.##########......# #.........# #.........# #...........##......# #.........# #.........# #...........##......# #.........# #.........# - #...........##......# #.........# ################.###### + #...........##..H...# #.........# ################.###### #...........##......# #.........# #.................############ #...........##......# ########.########.......#.........#..........# #...........##......# #...............#.......#.........#..........# diff --git a/squirrelbattle/assets/example_map_3.txt b/squirrelbattle/assets/example_map_3.txt new file mode 100644 index 0000000..c5dd8e3 --- /dev/null +++ b/squirrelbattle/assets/example_map_3.txt @@ -0,0 +1,41 @@ +1 6 +################################################################################ +#..............................................................................# +#..#...........................................................................# +#...........#..................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +#..............................................................................# +################################################################################ \ No newline at end of file diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index 37081cb..701f33e 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -22,13 +22,21 @@ class MapDisplay(Display): self.pack.tile_width * self.map.width + 1) def update_pad(self) -> None: - self.pad.resize(500, 500) - self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack), - self.pack.tile_fg_color, self.pack.tile_bg_color) + for 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.pack.entity_fg_color, self.pack.entity_bg_color) + 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 debug purposes # from squirrelbattle.entities.player import Player diff --git a/squirrelbattle/display/statsdisplay.py b/squirrelbattle/display/statsdisplay.py index 9af961c..35ff92c 100644 --- a/squirrelbattle/display/statsdisplay.py +++ b/squirrelbattle/display/statsdisplay.py @@ -23,16 +23,18 @@ class StatsDisplay(Display): self.player = game.player def update_pad(self) -> None: - string2 = _("player").capitalize() + " -- 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 {}\nCRI {}%"\ - .format(self.player.strength, - self.player.intelligence, self.player.charisma, - self.player.dexterity, self.player.constitution,\ - self.player.critical) + 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:") + " " @@ -57,15 +59,16 @@ class StatsDisplay(Display): if self.player.equipped_secondary: self.addstr(self.pad, 11, 0, _("Equipped secondary:") + " " - f"{self.pack[self.player.equipped_secondary.name.upper()]}") + + self.pack[self.player.equipped_secondary + .name.upper()]) if self.player.equipped_armor: self.addstr(self.pad, 12, 0, _("Equipped chestplate:") + " " - f"{self.pack[self.player.equipped_armor.name.upper()]}") + + self.pack[self.player.equipped_armor.name.upper()]) if self.player.equipped_helmet: self.addstr(self.pad, 13, 0, _("Equipped helmet:") + " " - f"{self.pack[self.player.equipped_helmet.name.upper()]}") + + self.pack[self.player.equipped_helmet.name.upper()]) self.addstr(self.pad, 14, 0, f"{self.pack.HAZELNUT} " f"x{self.player.hazel}") diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 10b9532..81302b3 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import curses -from typing import Any +from typing import Any, Union, Tuple class TexturePack: @@ -13,10 +13,11 @@ class TexturePack: 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 @@ -64,9 +65,10 @@ 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', @@ -76,6 +78,7 @@ TexturePack.ASCII_PACK = TexturePack( EMPTY=' ', EXPLOSION='%', FLOOR='.', + LADDER='H', HAZELNUT='¤', HEART='❤', HEDGEHOG='*', @@ -97,10 +100,11 @@ TexturePack.ASCII_PACK = TexturePack( 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='💣', @@ -109,6 +113,8 @@ TexturePack.SQUIRREL_PACK = TexturePack( EMPTY=' ', EXPLOSION='💥', FLOOR='██', + LADDER=('🪜', curses.COLOR_WHITE, (1000, 1000, 1000), + curses.COLOR_WHITE, (1000, 1000, 1000)), HAZELNUT='🌰', HEART='💜', HEDGEHOG='🦔', diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 1954204..0436e37 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -44,48 +44,17 @@ class Item(Entity): """ Indicates what should be done when the item is equipped. """ - if isinstance(self, Chestplate): - 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 - elif isinstance(self, Helmet): - 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 - elif isinstance(self, Weapon): - if self.held_by.equipped_main: - 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 - # For weapons, they are equipped as main only if main is empty. - else: - self.held_by.remove_from_inventory(self) - self.held_by.equipped_main = self - else: - # 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 + # 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 unequip(self) -> None: """ Indicates what should be done when the item is unequipped. """ - if isinstance(self, Chestplate): - self.held_by.equipped_armor = None - elif isinstance(self, Helmet): - self.held_by.equipped_helmet = None - elif isinstance(self, Weapon): - if self.held_by.equipped_main == self: - self.held_by.equipped_main = None - else: - self.held_by.equipped_secondary = None - else: - self.held_by.equipped_secondary = None + self.held_by.remove_from_inventory(self) self.held_by.add_to_inventory(self) def hold(self, holder: InventoryHolder) -> None: @@ -236,7 +205,6 @@ class Explosion(Item): """ The player can't hold an explosion. """ - pass class Weapon(Item): @@ -261,7 +229,8 @@ class Weapon(Item): """ When a weapon is equipped, the player gains strength. """ - super().equip() + self.held_by.remove_from_inventory(self) + self.held_by.equipped_main = self self.held_by.strength += self.damage def unequip(self) -> None: @@ -287,7 +256,7 @@ 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 @@ -305,33 +274,45 @@ class Armor(Item): 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,\ + def __init__(self, name: str = "shield", constitution: int = 2, price: int = 16, *args, **kwargs): - super().__init__(name=name, constitution=constitution, *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, \ + def __init__(self, name: str = "helmet", constitution: int = 2, price: int = 18, *args, **kwargs): - super().__init__(name=name, constitution=constitution, *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,\ + def __init__(self, name: str = "chestplate", constitution: int = 4, price: int = 30, *args, **kwargs): - super().__init__(name=name, constitution=constitution, *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): """ @@ -366,6 +347,7 @@ class BodySnatchPotion(Item): self.held_by.inventory.remove(self) + class Ring(Item): """ A class of rings that boost the player's statistics. @@ -379,9 +361,9 @@ class Ring(Item): 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,\ + 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 @@ -417,17 +399,26 @@ class Ring(Item): 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, \ + 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, \ + super().__init__(name=name, price=price, experience=experience, *args, **kwargs) diff --git a/squirrelbattle/entities/monsters.py b/squirrelbattle/entities/monsters.py index 39ce747..e22aa51 100644 --- a/squirrelbattle/entities/monsters.py +++ b/squirrelbattle/entities/monsters.py @@ -94,9 +94,11 @@ class Rabbit(Monster): A rabbit monster. """ def __init__(self, name: str = "rabbit", strength: int = 1, - maxhealth: int = 15, critical: int = 30, *args, **kwargs) -> None: + maxhealth: int = 15, critical: int = 30, + *args, **kwargs) -> None: super().__init__(name=name, strength=strength, - maxhealth=maxhealth, critical=critical, *args, **kwargs) + maxhealth=maxhealth, critical=critical, + *args, **kwargs) class TeddyBear(Monster): @@ -108,6 +110,7 @@ class TeddyBear(Monster): super().__init__(name=name, strength=strength, maxhealth=maxhealth, *args, **kwargs) + class GiantSeaEagle(Monster): """ An eagle boss diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index a36c6ef..615dfd5 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -26,10 +26,10 @@ class Player(InventoryHolder, FightingEntity): dexterity: int = 1, constitution: int = 1, level: int = 1, current_xp: int = 0, max_xp: int = 10, inventory: list = 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,\ - *args, **kwargs) -> 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, @@ -40,18 +40,15 @@ class Player(InventoryHolder, FightingEntity): self.inventory = self.translate_inventory(inventory or []) self.paths = dict() self.hazel = hazel - if isinstance(equipped_main, dict): - equipped_main = self.dict_to_item(equipped_main) - if isinstance(equipped_armor, dict): - equipped_armor = self.dict_to_item(equipped_armor) - if isinstance(equipped_secondary, dict): - equipped_secondary = self.dict_to_item(equipped_secondary) - if isinstance(equipped_helmet, dict): - equipped_helmet = self.dict_to_item(equipped_helmet) - self.equipped_main = equipped_main - self.equipped_armor = equipped_armor - self.equipped_secondary = equipped_secondary - self.equipped_helmet = equipped_helmet + 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: """ @@ -62,6 +59,7 @@ 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: """ @@ -82,7 +80,7 @@ class Player(InventoryHolder, FightingEntity): Adds some experience to the player. If the required amount is reached, the player levels up. """ - self.current_xp += int(xp*self.xp_buff) + self.current_xp += int(xp * self.xp_buff) self.level_up() def remove_from_inventory(self, obj: Item) -> None: diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index 2e1dd64..906d6df 100644 --- a/squirrelbattle/enums.py +++ b/squirrelbattle/enums.py @@ -47,6 +47,7 @@ class KeyValues(Enum): SPACE = auto() CHAT = auto() WAIT = auto() + LADDER = auto() @staticmethod def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]: @@ -81,4 +82,6 @@ class KeyValues(Enum): return KeyValues.CHAT elif key == settings.KEY_WAIT: return KeyValues.WAIT + elif key == settings.KEY_LADDER: + return KeyValues.LADDER return None diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 15b2b4b..ecdfccb 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -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 @@ -22,7 +22,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 @@ -52,7 +53,9 @@ class Game: Creates a new game on the screen. """ # TODO generate a new map procedurally - self.map = Map.load(ResourceManager.get_asset_path("example_map_2.txt")) + self.maps = [] + self.map_index = 0 + self.map = Map.load(ResourceManager.get_asset_path("example_map.txt")) self.map.logs = self.logs self.logs.clear() self.player = Player() @@ -61,6 +64,24 @@ class Game: self.map.spawn_random_entities(randint(3, 10)) self.inventory_menu.update_player(self.player) + @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. @@ -110,7 +131,7 @@ class Game: 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. """ @@ -130,7 +151,10 @@ class Game: self.state = GameMode.INVENTORY self.display_actions(DisplayActions.UPDATE) elif key == KeyValues.USE and self.player.equipped_main: - self.player.equipped_main.use() + 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: @@ -138,6 +162,54 @@ class Game: 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: """ diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 92c8aa8..f1f740b 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -2,13 +2,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later from enum import Enum, auto -from math import sqrt -from random import choice, randint +from math import ceil, sqrt +from random import choice, choices, randint from typing import List, Optional, Any, Dict, Tuple from queue import PriorityQueue from functools import reduce -from random import choice, randint, choices -from typing import List, Optional, Any from .display.texturepack import TexturePack from .translations import gettext as _ @@ -34,16 +32,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: """ 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 @@ -53,11 +82,16 @@ class Map: def __init__(self, width: int, height: int, tiles: list, start_y: int, start_x: int): + self.floor = 0 self.width = width self.height = height self.start_y = start_y self.start_x = start_x self.tiles = tiles + self.visibility = [[False for _ in range(len(tiles[0]))] + for _ in range(len(tiles))] + self.seen_tiles = [[False for _ in range(len(tiles[0]))] + for _ in range(len(tiles))] self.entities = [] self.logs = Logs() @@ -147,18 +181,138 @@ class Map: """ 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 = choices(Entity.get_all_entity_classes(),\ - weights = Entity.get_weights(), k=1)[0]() + entity = choices(Entity.get_all_entity_classes(), + weights=Entity.get_weights(), k=1)[0]() entity.move(y, x) self.add_entity(entity) + def compute_visibility(self, y: int, x: int, max_range: int) -> None: + """ + 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. @@ -210,6 +364,7 @@ class Tile(Enum): EMPTY = auto() WALL = auto() FLOOR = auto() + LADDER = auto() @staticmethod def from_ascii_char(ch: str) -> "Tile": @@ -226,7 +381,25 @@ class Tile(Enum): Translates a Tile to the corresponding character according 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: """ @@ -234,6 +407,12 @@ 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: """ Checks if an entity (player or not) can move in this tile. @@ -448,7 +627,7 @@ class Entity: from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \ TeddyBear, GiantSeaEagle from squirrelbattle.entities.friendly import Merchant, Sunflower, \ - Trumpet + Trumpet from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP return { @@ -525,12 +704,12 @@ class FightingEntity(Entity): The entity deals damage to the opponent based on their respective stats. """ - diceroll = randint(0, 100) + diceroll = randint(1, 100) damage = self.strength string = " " - if diceroll <= self.critical: # It is a critical hit + if diceroll <= self.critical: # It is a critical hit damage *= 4 - string = _(" It's a critical hit! ") + string = " " + _("It's a critical hit!") + " " return _("{name} hits {opponent}.")\ .format(name=_(self.translated_name.capitalize()), opponent=_(opponent.translated_name)) + string + \ diff --git a/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po index 29bf10e..20aa2a7 100644 --- a/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: squirrelbattle 3.14.1\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" -"POT-Creation-Date: 2020-12-12 18:02+0100\n" +"POT-Creation-Date: 2021-01-08 01:57+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,19 +17,44 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: squirrelbattle/display/menudisplay.py:139 +#, python-brace-format +msgid "{name} takes {amount} damage." +msgstr "{name} nimmt {amount} Schadenspunkte." + +#: squirrelbattle/display/menudisplay.py:160 msgid "INVENTORY" msgstr "BESTAND" -#: squirrelbattle/display/menudisplay.py:164 +#: squirrelbattle/display/menudisplay.py:202 msgid "STALL" msgstr "STAND" -#: squirrelbattle/display/statsdisplay.py:33 +#: squirrelbattle/display/statsdisplay.py:23 +#: squirrelbattle/tests/translations_test.py:60 +msgid "player" +msgstr "Spieler" + +#: squirrelbattle/display/statsdisplay.py:35 msgid "Inventory:" msgstr "Bestand:" #: squirrelbattle/display/statsdisplay.py:52 +msgid "Equipped main:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:56 +msgid "Equipped secondary:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:61 +msgid "Equipped chestplate:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:65 +msgid "Equipped helmet:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:72 msgid "YOU ARE DEAD" msgstr "SIE WURDEN GESTORBEN" @@ -49,20 +74,30 @@ 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:163 msgid "Bomb is exploding." msgstr "Die Bombe explodiert." -#: squirrelbattle/entities/items.py:248 +#: squirrelbattle/entities/items.py:344 #, python-brace-format msgid "{player} exchanged its body with {entity}." msgstr "{player} täuscht seinem Körper mit {entity} aus." -#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 +#: squirrelbattle/game.py:182 +#, python-brace-format +msgid "The player climbs down to the floor {floor}." +msgstr "Der Spieler klettert auf dem Stock {floor} hinunter." + +#: squirrelbattle/game.py:195 +#, python-brace-format +msgid "The player climbs up the floor {floor}." +msgstr "Der Spieler klettert auf dem Stock {floor} hinoben." + +#: squirrelbattle/game.py:285 squirrelbattle/tests/game_test.py:592 msgid "The buyer does not have enough money" msgstr "Der Kaufer hat nicht genug Geld" -#: squirrelbattle/game.py:249 +#: squirrelbattle/game.py:328 msgid "" "Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted." @@ -70,7 +105,7 @@ msgstr "" "In Ihrer Speicherdatei fehlen einige Schlüssel.\n" "Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht." -#: squirrelbattle/game.py:257 +#: squirrelbattle/game.py:336 msgid "" "No player was found on this map!\n" "Maybe you died?" @@ -78,7 +113,7 @@ msgstr "" "Auf dieser Karte wurde kein Spieler gefunden!\n" "Vielleicht sind Sie gestorben?" -#: squirrelbattle/game.py:277 +#: squirrelbattle/game.py:356 msgid "" "The JSON file is not correct.\n" "Your save seems corrupted. It got deleted." @@ -86,22 +121,26 @@ msgstr "" "Die JSON-Datei ist nicht korrekt.\n" "Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht." -#: squirrelbattle/interfaces.py:429 +#: squirrelbattle/interfaces.py:452 +msgid "It's a critical hit!" +msgstr "" + +#: squirrelbattle/interfaces.py:453 #, python-brace-format msgid "{name} hits {opponent}." msgstr "{name} schlägt {opponent}." -#: squirrelbattle/interfaces.py:441 +#: squirrelbattle/interfaces.py:465 #, 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:467 #, python-brace-format msgid "{name} dies." msgstr "{name} stirbt." -#: squirrelbattle/interfaces.py:477 +#: squirrelbattle/interfaces.py:501 #, python-brace-format msgid "{entity} said: {message}" msgstr "{entity} hat gesagt: {message}" @@ -110,8 +149,8 @@ msgstr "{entity} hat gesagt: {message}" msgid "Back" msgstr "Zurück" -#: squirrelbattle/tests/game_test.py:344 squirrelbattle/tests/game_test.py:347 -#: squirrelbattle/tests/game_test.py:350 squirrelbattle/tests/game_test.py:353 +#: squirrelbattle/tests/game_test.py:358 squirrelbattle/tests/game_test.py:361 +#: squirrelbattle/tests/game_test.py:364 squirrelbattle/tests/game_test.py:367 #: squirrelbattle/tests/translations_test.py:16 msgid "New game" msgstr "Neu Spiel" @@ -197,57 +236,57 @@ 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:57 +#: squirrelbattle/tests/translations_test.py:59 msgid "Language" msgstr "Sprache" -#: squirrelbattle/tests/translations_test.py:60 -msgid "player" -msgstr "Spieler" - -#: 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:64 +#: squirrelbattle/tests/translations_test.py:66 msgid "rabbit" msgstr "Kanninchen" -#: squirrelbattle/tests/translations_test.py:65 +#: squirrelbattle/tests/translations_test.py:67 msgid "sunflower" msgstr "Sonnenblume" -#: squirrelbattle/tests/translations_test.py:66 +#: squirrelbattle/tests/translations_test.py:68 msgid "teddy bear" msgstr "Teddybär" -#: squirrelbattle/tests/translations_test.py:67 +#: squirrelbattle/tests/translations_test.py:69 msgid "tiger" msgstr "Tiger" -#: squirrelbattle/tests/translations_test.py:69 +#: squirrelbattle/tests/translations_test.py:71 msgid "body snatch potion" msgstr "Leichenfleddererzaubertrank" -#: squirrelbattle/tests/translations_test.py:70 +#: squirrelbattle/tests/translations_test.py:72 msgid "bomb" msgstr "Bombe" -#: squirrelbattle/tests/translations_test.py:71 +#: squirrelbattle/tests/translations_test.py:73 msgid "explosion" msgstr "Explosion" -#: squirrelbattle/tests/translations_test.py:72 +#: squirrelbattle/tests/translations_test.py:74 msgid "heart" msgstr "Herz" -#: squirrelbattle/tests/translations_test.py:73 +#: squirrelbattle/tests/translations_test.py:75 msgid "sword" msgstr "schwert" diff --git a/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po index 8a7bc3e..0bfdac6 100644 --- a/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: squirrelbattle 3.14.1\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" -"POT-Creation-Date: 2020-12-12 18:02+0100\n" +"POT-Creation-Date: 2021-01-06 15:19+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,19 +17,44 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: squirrelbattle/display/menudisplay.py:139 +#, python-brace-format +msgid "{name} takes {amount} damage." +msgstr "{name} recibe {amount} daño." + +#: squirrelbattle/display/menudisplay.py:160 msgid "INVENTORY" msgstr "INVENTORIO" -#: squirrelbattle/display/menudisplay.py:164 +#: squirrelbattle/display/menudisplay.py:202 msgid "STALL" msgstr "PUESTO" -#: squirrelbattle/display/statsdisplay.py:33 +#: squirrelbattle/display/statsdisplay.py:23 +#: squirrelbattle/tests/translations_test.py:60 +msgid "player" +msgstr "jugador" + +#: squirrelbattle/display/statsdisplay.py:35 msgid "Inventory:" msgstr "Inventorio :" #: squirrelbattle/display/statsdisplay.py:52 +msgid "Equipped main:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:56 +msgid "Equipped secondary:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:61 +msgid "Equipped chestplate:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:65 +msgid "Equipped helmet:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:72 msgid "YOU ARE DEAD" msgstr "ERES MUERTO" @@ -48,20 +73,30 @@ 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:151 +#: squirrelbattle/entities/items.py:163 msgid "Bomb is exploding." msgstr "La bomba está explotando." -#: squirrelbattle/entities/items.py:248 +#: squirrelbattle/entities/items.py:344 #, python-brace-format msgid "{player} exchanged its body with {entity}." msgstr "{player} intercambió su cuerpo con {entity}." -#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 +#: squirrelbattle/game.py:182 +#, python-brace-format +msgid "The player climbs down to the floor {floor}." +msgstr "" + +#: squirrelbattle/game.py:195 +#, python-brace-format +msgid "The player climbs up the floor {floor}." +msgstr "" + +#: squirrelbattle/game.py:285 squirrelbattle/tests/game_test.py:592 msgid "The buyer does not have enough money" msgstr "El comprador no tiene suficiente dinero" -#: squirrelbattle/game.py:249 +#: squirrelbattle/game.py:328 msgid "" "Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted." @@ -69,7 +104,7 @@ msgstr "" "Algunas claves faltan en su archivo de guarda.\n" "Su guarda parece a ser corruptido. Fue eliminado." -#: squirrelbattle/game.py:257 +#: squirrelbattle/game.py:336 msgid "" "No player was found on this map!\n" "Maybe you died?" @@ -77,7 +112,7 @@ msgstr "" "No jugador encontrado sobre la carta !\n" "¿ Quizas murió ?" -#: squirrelbattle/game.py:277 +#: squirrelbattle/game.py:356 msgid "" "The JSON file is not correct.\n" "Your save seems corrupted. It got deleted." @@ -85,22 +120,26 @@ msgstr "" "El JSON archivo no es correcto.\n" "Su guarda parece corrupta. Fue eliminada." -#: squirrelbattle/interfaces.py:429 +#: squirrelbattle/interfaces.py:452 +msgid "It's a critical hit!" +msgstr "" + +#: squirrelbattle/interfaces.py:453 #, python-brace-format msgid "{name} hits {opponent}." msgstr "{name} golpea a {opponent}." -#: squirrelbattle/interfaces.py:441 +#: squirrelbattle/interfaces.py:465 #, python-brace-format -msgid "{name} takes {amount} damage." -msgstr "{name} recibe {amount} daño." +msgid "{name} takes {damage} damage." +msgstr "" -#: squirrelbattle/interfaces.py:443 +#: squirrelbattle/interfaces.py:467 #, python-brace-format msgid "{name} dies." msgstr "{name} se muere." -#: squirrelbattle/interfaces.py:477 +#: squirrelbattle/interfaces.py:501 #, python-brace-format msgid "{entity} said: {message}" msgstr "{entity} dijo : {message}" @@ -109,8 +148,8 @@ msgstr "{entity} dijo : {message}" msgid "Back" msgstr "Volver" -#: squirrelbattle/tests/game_test.py:344 squirrelbattle/tests/game_test.py:347 -#: squirrelbattle/tests/game_test.py:350 squirrelbattle/tests/game_test.py:353 +#: squirrelbattle/tests/game_test.py:358 squirrelbattle/tests/game_test.py:361 +#: squirrelbattle/tests/game_test.py:364 squirrelbattle/tests/game_test.py:367 #: squirrelbattle/tests/translations_test.py:16 msgid "New game" msgstr "Nuevo partido" @@ -196,57 +235,57 @@ 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:57 +#: squirrelbattle/tests/translations_test.py:59 msgid "Language" msgstr "Languaje" -#: squirrelbattle/tests/translations_test.py:60 -msgid "player" -msgstr "jugador" - -#: squirrelbattle/tests/translations_test.py:62 +#: squirrelbattle/tests/translations_test.py:64 msgid "hedgehog" msgstr "erizo" -#: squirrelbattle/tests/translations_test.py:63 +#: squirrelbattle/tests/translations_test.py:65 msgid "merchant" msgstr "comerciante" -#: squirrelbattle/tests/translations_test.py:64 +#: squirrelbattle/tests/translations_test.py:66 msgid "rabbit" msgstr "conejo" -#: squirrelbattle/tests/translations_test.py:65 +#: squirrelbattle/tests/translations_test.py:67 msgid "sunflower" msgstr "girasol" -#: squirrelbattle/tests/translations_test.py:66 +#: squirrelbattle/tests/translations_test.py:68 msgid "teddy bear" msgstr "osito de peluche" -#: squirrelbattle/tests/translations_test.py:67 +#: squirrelbattle/tests/translations_test.py:69 msgid "tiger" msgstr "tigre" -#: squirrelbattle/tests/translations_test.py:69 +#: squirrelbattle/tests/translations_test.py:71 msgid "body snatch potion" msgstr "poción de intercambio" -#: squirrelbattle/tests/translations_test.py:70 +#: squirrelbattle/tests/translations_test.py:72 msgid "bomb" msgstr "bomba" -#: squirrelbattle/tests/translations_test.py:71 +#: squirrelbattle/tests/translations_test.py:73 msgid "explosion" msgstr "explosión" -#: squirrelbattle/tests/translations_test.py:72 +#: squirrelbattle/tests/translations_test.py:74 msgid "heart" msgstr "corazón" -#: squirrelbattle/tests/translations_test.py:73 +#: squirrelbattle/tests/translations_test.py:75 msgid "sword" msgstr "espada" diff --git a/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po index ffbfdce..42172c3 100644 --- a/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po +++ b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: squirrelbattle 3.14.1\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" -"POT-Creation-Date: 2020-12-12 18:02+0100\n" +"POT-Creation-Date: 2021-01-06 15:19+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,19 +17,44 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: squirrelbattle/display/menudisplay.py:139 +#, python-brace-format +msgid "{name} takes {amount} damage." +msgstr "{name} prend {amount} points de dégât." + +#: squirrelbattle/display/menudisplay.py:160 msgid "INVENTORY" msgstr "INVENTAIRE" -#: squirrelbattle/display/menudisplay.py:164 +#: squirrelbattle/display/menudisplay.py:202 msgid "STALL" msgstr "STAND" -#: squirrelbattle/display/statsdisplay.py:33 +#: squirrelbattle/display/statsdisplay.py:23 +#: squirrelbattle/tests/translations_test.py:60 +msgid "player" +msgstr "joueur" + +#: squirrelbattle/display/statsdisplay.py:35 msgid "Inventory:" msgstr "Inventaire :" #: squirrelbattle/display/statsdisplay.py:52 +msgid "Equipped main:" +msgstr "Équipement principal :" + +#: squirrelbattle/display/statsdisplay.py:56 +msgid "Equipped secondary:" +msgstr "Équipement secondaire :" + +#: squirrelbattle/display/statsdisplay.py:61 +msgid "Equipped chestplate:" +msgstr "Plastron équipé :" + +#: squirrelbattle/display/statsdisplay.py:65 +msgid "Equipped helmet:" +msgstr "Casque équipé :" + +#: squirrelbattle/display/statsdisplay.py:72 msgid "YOU ARE DEAD" msgstr "VOUS ÊTES MORT" @@ -49,20 +74,30 @@ 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:163 msgid "Bomb is exploding." msgstr "La bombe explose." -#: squirrelbattle/entities/items.py:248 +#: squirrelbattle/entities/items.py:344 #, python-brace-format msgid "{player} exchanged its body with {entity}." msgstr "{player} a échangé son corps avec {entity}." -#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573 +#: squirrelbattle/game.py:182 +#, python-brace-format +msgid "The player climbs down to the floor {floor}." +msgstr "Le joueur descend à l'étage {floor}." + +#: squirrelbattle/game.py:195 +#, python-brace-format +msgid "The player climbs up the floor {floor}." +msgstr "Le joueur monte à l'étage {floor}." + +#: squirrelbattle/game.py:285 squirrelbattle/tests/game_test.py:592 msgid "The buyer does not have enough money" msgstr "L'acheteur n'a pas assez d'argent" -#: squirrelbattle/game.py:249 +#: squirrelbattle/game.py:328 msgid "" "Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted." @@ -70,7 +105,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:257 +#: squirrelbattle/game.py:336 msgid "" "No player was found on this map!\n" "Maybe you died?" @@ -78,7 +113,7 @@ msgstr "" "Aucun joueur n'a été trouvé sur la carte !\n" "Peut-être êtes-vous mort ?" -#: squirrelbattle/game.py:277 +#: squirrelbattle/game.py:356 msgid "" "The JSON file is not correct.\n" "Your save seems corrupted. It got deleted." @@ -86,22 +121,26 @@ 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:452 +msgid "It's a critical hit!" +msgstr "C'est un coup critique !" + +#: squirrelbattle/interfaces.py:453 #, python-brace-format msgid "{name} hits {opponent}." msgstr "{name} frappe {opponent}." -#: squirrelbattle/interfaces.py:441 +#: squirrelbattle/interfaces.py:465 #, 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:467 #, python-brace-format msgid "{name} dies." msgstr "{name} meurt." -#: squirrelbattle/interfaces.py:477 +#: squirrelbattle/interfaces.py:501 #, python-brace-format msgid "{entity} said: {message}" msgstr "{entity} a dit : {message}" @@ -110,8 +149,8 @@ msgstr "{entity} a dit : {message}" msgid "Back" msgstr "Retour" -#: squirrelbattle/tests/game_test.py:344 squirrelbattle/tests/game_test.py:347 -#: squirrelbattle/tests/game_test.py:350 squirrelbattle/tests/game_test.py:353 +#: squirrelbattle/tests/game_test.py:358 squirrelbattle/tests/game_test.py:361 +#: squirrelbattle/tests/game_test.py:364 squirrelbattle/tests/game_test.py:367 #: squirrelbattle/tests/translations_test.py:16 msgid "New game" msgstr "Nouvelle partie" @@ -197,57 +236,57 @@ 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:57 +#: squirrelbattle/tests/translations_test.py:59 msgid "Language" msgstr "Langue" -#: squirrelbattle/tests/translations_test.py:60 -msgid "player" -msgstr "joueur" - -#: 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:64 +#: squirrelbattle/tests/translations_test.py:66 msgid "rabbit" msgstr "lapin" -#: squirrelbattle/tests/translations_test.py:65 +#: squirrelbattle/tests/translations_test.py:67 msgid "sunflower" msgstr "tournesol" -#: squirrelbattle/tests/translations_test.py:66 +#: squirrelbattle/tests/translations_test.py:68 msgid "teddy bear" msgstr "nounours" -#: squirrelbattle/tests/translations_test.py:67 +#: squirrelbattle/tests/translations_test.py:69 msgid "tiger" msgstr "tigre" -#: squirrelbattle/tests/translations_test.py:69 +#: squirrelbattle/tests/translations_test.py:71 msgid "body snatch potion" msgstr "potion d'arrachage de corps" -#: squirrelbattle/tests/translations_test.py:70 +#: squirrelbattle/tests/translations_test.py:72 msgid "bomb" msgstr "bombe" -#: squirrelbattle/tests/translations_test.py:71 +#: squirrelbattle/tests/translations_test.py:73 msgid "explosion" -msgstr "" +msgstr "explosion" -#: squirrelbattle/tests/translations_test.py:72 +#: squirrelbattle/tests/translations_test.py:74 msgid "heart" msgstr "cœur" -#: squirrelbattle/tests/translations_test.py:73 +#: squirrelbattle/tests/translations_test.py:75 msgid "sword" msgstr "épée" diff --git a/squirrelbattle/settings.py b/squirrelbattle/settings.py index 58e8cc1..b5e2c14 100644 --- a/squirrelbattle/settings.py +++ b/squirrelbattle/settings.py @@ -34,6 +34,7 @@ class Settings: 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'] diff --git a/squirrelbattle/tests/entities_test.py b/squirrelbattle/tests/entities_test.py index a7843f7..af6f7dd 100644 --- a/squirrelbattle/tests/entities_test.py +++ b/squirrelbattle/tests/entities_test.py @@ -78,6 +78,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) @@ -177,7 +178,7 @@ 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) diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index b51a46a..0843ae8 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -2,13 +2,16 @@ # SPDX-License-Identifier: GPL-3.0-or-later import os +import random import unittest 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, Explosion +from ..entities.items import Bomb, Heart, Sword, Explosion, Shield, Helmet, \ + Chestplate, RingCritical +from ..entities.monsters import GiantSeaEagle from ..entities.player import Player from ..enums import DisplayActions from ..game import Game, KeyValues, GameMode @@ -46,11 +49,6 @@ class TestGame(unittest.TestCase): bomb.hold(self.game.player) sword.hold(self.game.player) - for entity in self.game.map.entities: - # trumpets change order when they are loaded, this breaks the test. - if entity.name == 'trumpet': - self.game.map.remove_entity(entity) - # Ensure that merchants can be saved merchant = Merchant() merchant.move(3, 6) @@ -153,6 +151,9 @@ class TestGame(unittest.TestCase): 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), @@ -343,7 +344,7 @@ class TestGame(unittest.TestCase): self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a') # Navigate to "texture pack" - for ignored in range(11): + for ignored in range(12): self.game.handle_key_pressed(KeyValues.DOWN) # Change texture pack @@ -615,6 +616,131 @@ class TestGame(unittest.TestCase): 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_critical_hit(self) -> None: + """ + Ensure that critical hits are working. + """ + random.seed(2) # Next random.randint(1, 100) will output 8 + self.game.player.critical = 10 + sea_eagle = GiantSeaEagle() + self.game.map.add_entity(sea_eagle) + sea_eagle.move(2, 6) + old_health = sea_eagle.health + self.game.player.hit(sea_eagle) + self.assertEqual(sea_eagle.health, + old_health - self.game.player.strength * 4) + + 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. diff --git a/squirrelbattle/tests/interfaces_test.py b/squirrelbattle/tests/interfaces_test.py index 6f2a4bb..df1cbea 100644 --- a/squirrelbattle/tests/interfaces_test.py +++ b/squirrelbattle/tests/interfaces_test.py @@ -4,7 +4,7 @@ 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 @@ -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) diff --git a/squirrelbattle/tests/translations_test.py b/squirrelbattle/tests/translations_test.py index 72b8562..4a99e73 100644 --- a/squirrelbattle/tests/translations_test.py +++ b/squirrelbattle/tests/translations_test.py @@ -53,6 +53,8 @@ class TestTranslations(unittest.TestCase): 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")