Merge branch 'master' into 'doc'

# Conflicts:
#   squirrelbattle/entities/items.py
#   squirrelbattle/interfaces.py
This commit is contained in:
eichhornchen 2021-01-08 11:10:09 +01:00
commit 196e3708d2
21 changed files with 802 additions and 232 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
env/
venv/
local/
.coverage
.pytest_cache/

View File

@ -1,8 +1,8 @@
1 6
####### #############
#.....# #...........#
#.H...# #...........#
#.....# #####...........#
#.....# #...............#
#.....# #............H..#
#.##### #.###...........#
#.# #.# #...........#
#.# #.# #############

View File

@ -1,6 +1,6 @@
1 17
########### #########
#.........# #.......#
#....H....# #.......#
#.........# ############.......#
#.........###############..........#.......##############
#.........#........................#....................#
@ -13,7 +13,7 @@
########.##########......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# ################.######
#...........##..H...# #.........# ################.######
#...........##......# #.........# #.................############
#...........##......# ########.########.......#.........#..........#
#...........##......# #...............#.......#.........#..........#

View File

@ -0,0 +1,41 @@
1 6
################################################################################
#..............................................................................#
#..#...........................................................................#
#...........#..................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
################################################################################

View File

@ -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

View File

@ -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}")

View File

@ -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='🦔',

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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:
"""

View File

@ -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 + \

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View File

@ -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']

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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")