Merge branch 'equipment' into 'master'

Equipment

Closes #19, #30, #48, #51 et #52

See merge request ynerant/squirrel-battle!54
This commit is contained in:
ynerant 2021-01-08 02:19:29 +01:00
commit fb47c15d6b
12 changed files with 545 additions and 74 deletions

View File

@ -33,7 +33,8 @@ class StatsDisplay(Display):
f"INT {self.player.intelligence}\n" \ f"INT {self.player.intelligence}\n" \
f"CHR {self.player.charisma}\n" \ f"CHR {self.player.charisma}\n" \
f"DEX {self.player.dexterity}\n" \ f"DEX {self.player.dexterity}\n" \
f"CON {self.player.constitution}" f"CON {self.player.constitution}\n" \
f"CRI {self.player.critical}%"
self.addstr(self.pad, 3, 0, string3) self.addstr(self.pad, 3, 0, string3)
inventory_str = _("Inventory:") + " " inventory_str = _("Inventory:") + " "
@ -49,13 +50,31 @@ class StatsDisplay(Display):
if count > 1: if count > 1:
inventory_str += f"x{count} " inventory_str += f"x{count} "
printed_items.append(item) printed_items.append(item)
self.addstr(self.pad, 8, 0, inventory_str) self.addstr(self.pad, 9, 0, inventory_str)
self.addstr(self.pad, 9, 0, f"{self.pack.HAZELNUT} " if self.player.equipped_main:
f"x{self.player.hazel}") self.addstr(self.pad, 10, 0,
_("Equipped main:") + " "
f"{self.pack[self.player.equipped_main.name.upper()]}")
if self.player.equipped_secondary:
self.addstr(self.pad, 11, 0,
_("Equipped secondary:") + " "
+ self.pack[self.player.equipped_secondary
.name.upper()])
if self.player.equipped_armor:
self.addstr(self.pad, 12, 0,
_("Equipped chestplate:") + " "
+ self.pack[self.player.equipped_armor.name.upper()])
if self.player.equipped_helmet:
self.addstr(self.pad, 13, 0,
_("Equipped helmet:") + " "
+ self.pack[self.player.equipped_helmet.name.upper()])
self.addstr(self.pad, 14, 0, f"{self.pack.HAZELNUT} "
f"x{self.player.hazel}")
if self.player.dead: if self.player.dead:
self.addstr(self.pad, 11, 0, _("YOU ARE DEAD"), curses.COLOR_RED, self.addstr(self.pad, 15, 0, _("YOU ARE DEAD"), curses.COLOR_RED,
bold=True, blink=True, standout=True) bold=True, blink=True, standout=True)
def display(self) -> None: def display(self) -> None:

View File

@ -35,6 +35,12 @@ class TexturePack:
TIGER: str TIGER: str
TRUMPET: str TRUMPET: str
WALL: str WALL: str
EAGLE: str
SHIELD: str
CHESTPLATE: str
HELMET: str
RING_OF_MORE_EXPERIENCE: str
RING_OF_CRITICAL_DAMAGE: str
ASCII_PACK: "TexturePack" ASCII_PACK: "TexturePack"
SQUIRREL_PACK: "TexturePack" SQUIRREL_PACK: "TexturePack"
@ -66,7 +72,7 @@ TexturePack.ASCII_PACK = TexturePack(
entity_bg_color=curses.COLOR_BLACK, entity_bg_color=curses.COLOR_BLACK,
BODY_SNATCH_POTION='S', BODY_SNATCH_POTION='S',
BOMB='o', BOMB='ç',
EMPTY=' ', EMPTY=' ',
EXPLOSION='%', EXPLOSION='%',
FLOOR='.', FLOOR='.',
@ -77,12 +83,18 @@ TexturePack.ASCII_PACK = TexturePack(
MERCHANT='M', MERCHANT='M',
PLAYER='@', PLAYER='@',
RABBIT='Y', RABBIT='Y',
SHIELD='D',
SUNFLOWER='I', SUNFLOWER='I',
SWORD='\u2020', SWORD='\u2020',
TEDDY_BEAR='8', TEDDY_BEAR='8',
TIGER='n', TIGER='n',
TRUMPET='/', TRUMPET='/',
WALL='#', WALL='#',
EAGLE='µ',
CHESTPLATE='(',
HELMET='0',
RING_OF_MORE_EXPERIENCE='o',
RING_OF_CRITICAL_DAMAGE='o',
) )
TexturePack.SQUIRREL_PACK = TexturePack( TexturePack.SQUIRREL_PACK = TexturePack(
@ -107,10 +119,16 @@ TexturePack.SQUIRREL_PACK = TexturePack(
PLAYER='🐿️ ', PLAYER='🐿️ ',
MERCHANT='🦜', MERCHANT='🦜',
RABBIT='🐇', RABBIT='🐇',
SHIELD='🛡️ ',
SUNFLOWER='🌻', SUNFLOWER='🌻',
SWORD='🗡️ ', SWORD='🗡️ ',
TEDDY_BEAR='🧸', TEDDY_BEAR='🧸',
TIGER='🐅', TIGER='🐅',
TRUMPET='🎺', TRUMPET='🎺',
WALL='🧱', WALL='🧱',
EAGLE='🦅',
CHESTPLATE='🦺',
HELMET='⛑️',
RING_OF_MORE_EXPERIENCE='💍',
RING_OF_CRITICAL_DAMAGE='💍',
) )

View File

@ -4,7 +4,6 @@
from random import choice, randint from random import choice, randint
from typing import Optional from typing import Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map, InventoryHolder from ..interfaces import Entity, FightingEntity, Map, InventoryHolder
from ..translations import gettext as _ from ..translations import gettext as _
@ -30,7 +29,7 @@ class Item(Entity):
The item is dropped from the inventory onto the floor. The item is dropped from the inventory onto the floor.
""" """
if self.held: if self.held:
self.held_by.inventory.remove(self) self.held_by.remove_from_inventory(self)
self.held_by.map.add_entity(self) self.held_by.map.add_entity(self)
self.move(self.held_by.y, self.held_by.x) self.move(self.held_by.y, self.held_by.x)
self.held = False self.held = False
@ -45,15 +44,27 @@ class Item(Entity):
""" """
Indicates what should be done when the item is equipped. Indicates what should be done when the item is equipped.
""" """
# Other objects are only equipped as secondary.
if self.held_by.equipped_secondary:
self.held_by.equipped_secondary.unequip()
self.held_by.remove_from_inventory(self)
self.held_by.equipped_secondary = self
def hold(self, player: InventoryHolder) -> None: def unequip(self) -> None:
"""
Indicates what should be done when the item is unequipped.
"""
self.held_by.remove_from_inventory(self)
self.held_by.add_to_inventory(self)
def hold(self, holder: InventoryHolder) -> None:
""" """
The item is taken from the floor and put into the inventory. The item is taken from the floor and put into the inventory.
""" """
self.held = True self.held = True
self.held_by = player self.held_by = holder
self.held_by.map.remove_entity(self) self.held_by.map.remove_entity(self)
player.add_to_inventory(self) holder.add_to_inventory(self)
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
@ -68,7 +79,8 @@ class Item(Entity):
""" """
Returns the list of all item classes. Returns the list of all item classes.
""" """
return [BodySnatchPotion, Bomb, Heart, Sword] return [BodySnatchPotion, Bomb, Heart, Shield, Sword,
Chestplate, Helmet, RingCritical, RingXP]
def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool:
""" """
@ -120,7 +132,7 @@ class Bomb(Item):
""" """
damage: int = 5 damage: int = 5
exploding: bool exploding: bool
owner: Optional["Player"] owner: Optional["InventoryHolder"]
tick: int tick: int
def __init__(self, name: str = "bomb", damage: int = 5, def __init__(self, name: str = "bomb", damage: int = 5,
@ -193,7 +205,6 @@ class Explosion(Item):
""" """
The player can't hold an explosion. The player can't hold an explosion.
""" """
pass
class Weapon(Item): class Weapon(Item):
@ -214,14 +225,99 @@ class Weapon(Item):
d["damage"] = self.damage d["damage"] = self.damage
return d return d
def equip(self) -> None:
"""
When a weapon is equipped, the player gains strength.
"""
self.held_by.remove_from_inventory(self)
self.held_by.equipped_main = self
self.held_by.strength += self.damage
def unequip(self) -> None:
"""
Remove the strength earned by the weapon.
:return:
"""
super().unequip()
self.held_by.strength -= self.damage
class Sword(Weapon): class Sword(Weapon):
""" """
A basic weapon A basic weapon
""" """
def __init__(self, name: str = "sword", price: int = 20, *args, **kwargs): def __init__(self, name: str = "sword", price: int = 20,
*args, **kwargs):
super().__init__(name=name, price=price, *args, **kwargs) super().__init__(name=name, price=price, *args, **kwargs)
self.name = name
class Armor(Item):
"""
Class of items that increase the player's constitution.
"""
constitution: int
def __init__(self, constitution: int, *args, **kwargs):
super().__init__(*args, **kwargs)
self.constitution = constitution
def equip(self) -> None:
super().equip()
self.held_by.constitution += self.constitution
def unequip(self) -> None:
super().unequip()
self.held_by.constitution -= self.constitution
def save_state(self) -> dict:
d = super().save_state()
d["constitution"] = self.constitution
return d
class Shield(Armor):
"""
Class of shield items, they can be equipped in the other hand.
"""
def __init__(self, name: str = "shield", constitution: int = 2,
price: int = 6, *args, **kwargs):
super().__init__(name=name, constitution=constitution, price=price,
*args, **kwargs)
class Helmet(Armor):
"""
Class of helmet items, they can be equipped on the head.
"""
def __init__(self, name: str = "helmet", constitution: int = 2,
price: int = 8, *args, **kwargs):
super().__init__(name=name, constitution=constitution, price=price,
*args, **kwargs)
def equip(self) -> None:
if self.held_by.equipped_helmet:
self.held_by.equipped_helmet.unequip()
self.held_by.remove_from_inventory(self)
self.held_by.equipped_helmet = self
class Chestplate(Armor):
"""
Class of chestplate items, they can be equipped on the body.
"""
def __init__(self, name: str = "chestplate", constitution: int = 4,
price: int = 15, *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): class BodySnatchPotion(Item):
@ -256,3 +352,79 @@ class BodySnatchPotion(Item):
self.held_by.recalculate_paths() self.held_by.recalculate_paths()
self.held_by.inventory.remove(self) self.held_by.inventory.remove(self)
class Ring(Item):
"""
A class of rings that boost the player's statistics.
"""
maxhealth: int
strength: int
intelligence: int
charisma: int
dexterity: int
constitution: int
critical: int
experience: float
def __init__(self, maxhealth: int = 0, strength: int = 0,
intelligence: int = 0, charisma: int = 0,
dexterity: int = 0, constitution: int = 0,
critical: int = 0, experience: float = 0, *args, **kwargs):
super().__init__(*args, **kwargs)
self.maxhealth = maxhealth
self.strength = strength
self.intelligence = intelligence
self.charisma = charisma
self.dexterity = dexterity
self.constitution = constitution
self.critical = critical
self.experience = experience
def equip(self) -> None:
super().equip()
self.held_by.maxhealth += self.maxhealth
self.held_by.strength += self.strength
self.held_by.intelligence += self.intelligence
self.held_by.charisma += self.charisma
self.held_by.dexterity += self.dexterity
self.held_by.constitution += self.constitution
self.held_by.critical += self.critical
self.held_by.xp_buff += self.experience
def unequip(self) -> None:
super().unequip()
self.held_by.maxhealth -= self.maxhealth
self.held_by.strength -= self.strength
self.held_by.intelligence -= self.intelligence
self.held_by.charisma -= self.charisma
self.held_by.dexterity -= self.dexterity
self.held_by.constitution -= self.constitution
self.held_by.critical -= self.critical
self.held_by.xp_buff -= self.experience
def save_state(self) -> dict:
d = super().save_state()
d["maxhealth"] = self.maxhealth
d["strength"] = self.strength
d["intelligence"] = self.intelligence
d["charisma"] = self.charisma
d["dexterity"] = self.dexterity
d["constitution"] = self.constitution
d["critical"] = self.critical
d["experience"] = self.experience
return d
class RingCritical(Ring):
def __init__(self, name: str = "ring_of_critical_damage", price: int = 15,
critical: int = 20, *args, **kwargs):
super().__init__(name=name, price=price, critical=critical,
*args, **kwargs)
class RingXP(Ring):
def __init__(self, name: str = "ring_of_more_experience", price: int = 25,
experience: float = 2, *args, **kwargs):
super().__init__(name=name, price=price, experience=experience,
*args, **kwargs)

View File

@ -94,9 +94,11 @@ class Rabbit(Monster):
A rabbit monster. A rabbit monster.
""" """
def __init__(self, name: str = "rabbit", strength: int = 1, def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, *args, **kwargs) -> None: maxhealth: int = 15, critical: int = 30,
*args, **kwargs) -> None:
super().__init__(name=name, strength=strength, super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs) maxhealth=maxhealth, critical=critical,
*args, **kwargs)
class TeddyBear(Monster): class TeddyBear(Monster):
@ -107,3 +109,13 @@ class TeddyBear(Monster):
maxhealth: int = 50, *args, **kwargs) -> None: maxhealth: int = 50, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength, super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs) maxhealth=maxhealth, *args, **kwargs)
class GiantSeaEagle(Monster):
"""
An eagle boss
"""
def __init__(self, name: str = "eagle", strength: int = 1000,
maxhealth: int = 5000, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -2,7 +2,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from random import randint from random import randint
from typing import Dict, Optional, Tuple
from .items import Item
from ..interfaces import FightingEntity, InventoryHolder from ..interfaces import FightingEntity, InventoryHolder
@ -12,22 +14,40 @@ class Player(InventoryHolder, FightingEntity):
""" """
current_xp: int = 0 current_xp: int = 0
max_xp: int = 10 max_xp: int = 10
xp_buff: float = 1
paths: Dict[Tuple[int, int], Tuple[int, int]]
equipped_main: Optional[Item]
equipped_secondary: Optional[Item]
equipped_helmet: Optional[Item]
equipped_armor: Optional[Item]
def __init__(self, name: str = "player", maxhealth: int = 20, def __init__(self, name: str = "player", maxhealth: int = 20,
strength: int = 5, intelligence: int = 1, charisma: int = 1, strength: int = 5, intelligence: int = 1, charisma: int = 1,
dexterity: int = 1, constitution: int = 1, level: int = 1, dexterity: int = 1, constitution: int = 1, level: int = 1,
current_xp: int = 0, max_xp: int = 10, inventory: list = None, current_xp: int = 0, max_xp: int = 10, inventory: list = None,
hazel: int = 42, vision: int = 5, *args, **kwargs) \ hazel: int = 42, equipped_main: Optional[Item] = None,
-> 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, super().__init__(name=name, maxhealth=maxhealth, strength=strength,
intelligence=intelligence, charisma=charisma, intelligence=intelligence, charisma=charisma,
dexterity=dexterity, constitution=constitution, dexterity=dexterity, constitution=constitution,
level=level, *args, **kwargs) level=level, critical=critical, *args, **kwargs)
self.current_xp = current_xp self.current_xp = current_xp
self.max_xp = max_xp self.max_xp = max_xp
self.xp_buff = xp_buff
self.inventory = self.translate_inventory(inventory or []) self.inventory = self.translate_inventory(inventory or [])
self.paths = dict() self.paths = dict()
self.hazel = hazel self.hazel = hazel
self.equipped_main = self.dict_to_item(equipped_main) \
if isinstance(equipped_main, dict) else equipped_main
self.equipped_armor = self.dict_to_item(equipped_armor) \
if isinstance(equipped_armor, dict) else equipped_armor
self.equipped_secondary = self.dict_to_item(equipped_secondary) \
if isinstance(equipped_secondary, dict) else equipped_secondary
self.equipped_helmet = self.dict_to_item(equipped_helmet) \
if isinstance(equipped_helmet, dict) else equipped_helmet
self.vision = vision self.vision = vision
def move(self, y: int, x: int) -> None: def move(self, y: int, x: int) -> None:
@ -60,9 +80,24 @@ class Player(InventoryHolder, FightingEntity):
Adds some experience to the player. Adds some experience to the player.
If the required amount is reached, the player levels up. If the required amount is reached, the player levels up.
""" """
self.current_xp += xp self.current_xp += int(xp * self.xp_buff)
self.level_up() self.level_up()
def remove_from_inventory(self, obj: Item) -> None:
"""
Remove the given item from the inventory, even if the item is equipped.
"""
if obj == self.equipped_main:
self.equipped_main = None
elif obj == self.equipped_armor:
self.equipped_armor = None
elif obj == self.equipped_secondary:
self.equipped_secondary = None
elif obj == self.equipped_helmet:
self.equipped_helmet = None
else:
return super().remove_from_inventory(obj)
# noinspection PyTypeChecker,PyUnresolvedReferences # noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \ def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool: -> bool:
@ -92,4 +127,12 @@ class Player(InventoryHolder, FightingEntity):
d = super().save_state() d = super().save_state()
d["current_xp"] = self.current_xp d["current_xp"] = self.current_xp
d["max_xp"] = self.max_xp d["max_xp"] = self.max_xp
d["equipped_main"] = self.equipped_main.save_state()\
if self.equipped_main else None
d["equipped_armor"] = self.equipped_armor.save_state()\
if self.equipped_armor else None
d["equipped_secondary"] = self.equipped_secondary.save_state()\
if self.equipped_secondary else None
d["equipped_helmet"] = self.equipped_helmet.save_state()\
if self.equipped_helmet else None
return d return d

View File

@ -131,7 +131,7 @@ class Game:
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.display_actions(DisplayActions.REFRESH) 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. In play mode, arrows or zqsd move the main character.
""" """
@ -149,6 +149,12 @@ class Game:
self.map.tick(self.player) self.map.tick(self.player)
elif key == KeyValues.INVENTORY: elif key == KeyValues.INVENTORY:
self.state = GameMode.INVENTORY self.state = GameMode.INVENTORY
self.display_actions(DisplayActions.UPDATE)
elif key == KeyValues.USE and self.player.equipped_main:
if self.player.equipped_main:
self.player.equipped_main.use()
if self.player.equipped_secondary:
self.player.equipped_secondary.use()
elif key == KeyValues.SPACE: elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
elif key == KeyValues.CHAT: elif key == KeyValues.CHAT:

View File

@ -3,7 +3,7 @@
from enum import Enum, auto from enum import Enum, auto
from math import ceil, sqrt from math import ceil, sqrt
from random import choice, randint from random import choice, choices, randint
from typing import List, Optional, Any, Dict, Tuple from typing import List, Optional, Any, Dict, Tuple
from queue import PriorityQueue from queue import PriorityQueue
from functools import reduce from functools import reduce
@ -188,7 +188,8 @@ class Map:
tile = self.tiles[y][x] tile = self.tiles[y][x]
if tile.can_walk(): if tile.can_walk():
break break
entity = choice(Entity.get_all_entity_classes())() entity = choices(Entity.get_all_entity_classes(),
weights=Entity.get_weights(), k=1)[0]()
entity.move(y, x) entity.move(y, x)
self.add_entity(entity) self.add_entity(entity)
@ -602,11 +603,19 @@ class Entity:
""" """
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear Rabbit, TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Merchant, Sunflower, \ from squirrelbattle.entities.friendly import Merchant, Sunflower
Trumpet
return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear, return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,
Sunflower, Tiger, Merchant, Trumpet] Sunflower, Tiger, Merchant, GiantSeaEagle]
@staticmethod
def get_weights() -> list:
"""
Returns a weigth list associated to the above function, to
be used to spawn random entities with a certain probability.
"""
return [3, 5, 6, 5, 5, 5,
5, 4, 4, 1]
@staticmethod @staticmethod
def get_all_entity_classes_in_a_dict() -> dict: def get_all_entity_classes_in_a_dict() -> dict:
@ -615,11 +624,11 @@ class Entity:
""" """
from squirrelbattle.entities.player import Player from squirrelbattle.entities.player import Player
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Merchant, Sunflower, \ from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet Trumpet
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \
Heart, Sword Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP
return { return {
"Tiger": Tiger, "Tiger": Tiger,
"Bomb": Bomb, "Bomb": Bomb,
@ -633,6 +642,12 @@ class Entity:
"Sunflower": Sunflower, "Sunflower": Sunflower,
"Sword": Sword, "Sword": Sword,
"Trumpet": Trumpet, "Trumpet": Trumpet,
"Eagle": GiantSeaEagle,
"Shield": Shield,
"Chestplate": Chestplate,
"Helmet": Helmet,
"RingCritical": RingCritical,
"RingXP": RingXP,
} }
def save_state(self) -> dict: def save_state(self) -> dict:
@ -659,11 +674,12 @@ class FightingEntity(Entity):
dexterity: int dexterity: int
constitution: int constitution: int
level: int level: int
critical: int
def __init__(self, maxhealth: int = 0, health: Optional[int] = None, def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
strength: int = 0, intelligence: int = 0, charisma: int = 0, strength: int = 0, intelligence: int = 0, charisma: int = 0,
dexterity: int = 0, constitution: int = 0, level: int = 0, dexterity: int = 0, constitution: int = 0, level: int = 0,
*args, **kwargs) -> None: critical: int = 0, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.maxhealth = maxhealth self.maxhealth = maxhealth
self.health = maxhealth if health is None else health self.health = maxhealth if health is None else health
@ -673,6 +689,7 @@ class FightingEntity(Entity):
self.dexterity = dexterity self.dexterity = dexterity
self.constitution = constitution self.constitution = constitution
self.level = level self.level = level
self.critical = critical
@property @property
def dead(self) -> bool: def dead(self) -> bool:
@ -686,21 +703,28 @@ class FightingEntity(Entity):
The entity deals damage to the opponent The entity deals damage to the opponent
based on their respective stats. based on their respective stats.
""" """
diceroll = randint(1, 100)
damage = self.strength
string = " "
if diceroll <= self.critical: # It is a critical hit
damage *= 4
string = " " + _("It's a critical hit!") + " "
return _("{name} hits {opponent}.")\ return _("{name} hits {opponent}.")\
.format(name=_(self.translated_name.capitalize()), .format(name=_(self.translated_name.capitalize()),
opponent=_(opponent.translated_name)) + " " + \ opponent=_(opponent.translated_name)) + string + \
opponent.take_damage(self, self.strength) opponent.take_damage(self, damage)
def take_damage(self, attacker: "Entity", amount: int) -> str: def take_damage(self, attacker: "Entity", amount: int) -> str:
""" """
The entity takes damage from the attacker The entity takes damage from the attacker
based on their respective stats. based on their respective stats.
""" """
self.health -= amount damage = max(0, amount - self.constitution)
self.health -= damage
if self.health <= 0: if self.health <= 0:
self.die() self.die()
return _("{name} takes {amount} damage.")\ return _("{name} takes {damage} damage.")\
.format(name=self.translated_name.capitalize(), amount=str(amount))\ .format(name=self.translated_name.capitalize(), damage=str(damage))\
+ (" " + _("{name} dies.") + (" " + _("{name} dies.")
.format(name=self.translated_name.capitalize()) .format(name=self.translated_name.capitalize())
if self.health <= 0 else "") if self.health <= 0 else "")
@ -757,10 +781,10 @@ class InventoryHolder(Entity):
""" """
for i in range(len(inventory)): for i in range(len(inventory)):
if isinstance(inventory[i], dict): if isinstance(inventory[i], dict):
inventory[i] = self.dict_to_inventory(inventory[i]) inventory[i] = self.dict_to_item(inventory[i])
return inventory return inventory
def dict_to_inventory(self, item_dict: dict) -> Entity: def dict_to_item(self, item_dict: dict) -> Entity:
""" """
Translates a dictionnary that contains the state of an item Translates a dictionnary that contains the state of an item
into an item object. into an item object.
@ -783,13 +807,15 @@ class InventoryHolder(Entity):
""" """
Adds an object to the inventory. Adds an object to the inventory.
""" """
self.inventory.append(obj) if obj not in self.inventory:
self.inventory.append(obj)
def remove_from_inventory(self, obj: Any) -> None: def remove_from_inventory(self, obj: Any) -> None:
""" """
Removes an object from the inventory. Removes an object from the inventory.
""" """
self.inventory.remove(obj) if obj in self.inventory:
self.inventory.remove(obj)
def change_hazel_balance(self, hz: int) -> None: def change_hazel_balance(self, hz: int) -> None:
""" """

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n" "Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2021-01-06 15:19+0100\n" "POT-Creation-Date: 2021-01-08 01:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,6 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} nimmt {amount} Schadenspunkte."
#: squirrelbattle/display/menudisplay.py:160 #: squirrelbattle/display/menudisplay.py:160
msgid "INVENTORY" msgid "INVENTORY"
msgstr "BESTAND" msgstr "BESTAND"
@ -25,11 +29,32 @@ msgstr "BESTAND"
msgid "STALL" msgid "STALL"
msgstr "STAND" msgstr "STAND"
#: squirrelbattle/display/statsdisplay.py:36 #: squirrelbattle/display/statsdisplay.py:23
#: squirrelbattle/tests/translations_test.py:60
msgid "player"
msgstr "Spieler"
#: squirrelbattle/display/statsdisplay.py:35
msgid "Inventory:" msgid "Inventory:"
msgstr "Bestand:" msgstr "Bestand:"
#: squirrelbattle/display/statsdisplay.py:55 #: 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" msgid "YOU ARE DEAD"
msgstr "SIE WURDEN GESTORBEN" msgstr "SIE WURDEN GESTORBEN"
@ -49,11 +74,11 @@ msgstr "Die Sonne ist warm heute"
#. The bomb is exploding. #. The bomb is exploding.
#. Each entity that is close to the bomb takes damages. #. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed. #. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:151 #: squirrelbattle/entities/items.py:163
msgid "Bomb is exploding." msgid "Bomb is exploding."
msgstr "Die Bombe explodiert." msgstr "Die Bombe explodiert."
#: squirrelbattle/entities/items.py:248 #: squirrelbattle/entities/items.py:344
#, python-brace-format #, python-brace-format
msgid "{player} exchanged its body with {entity}." msgid "{player} exchanged its body with {entity}."
msgstr "{player} täuscht seinem Körper mit {entity} aus." msgstr "{player} täuscht seinem Körper mit {entity} aus."
@ -96,6 +121,10 @@ msgstr ""
"Die JSON-Datei ist nicht korrekt.\n" "Die JSON-Datei ist nicht korrekt.\n"
"Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht." "Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht."
#: squirrelbattle/interfaces.py:452
msgid "It's a critical hit!"
msgstr ""
#: squirrelbattle/interfaces.py:453 #: squirrelbattle/interfaces.py:453
#, python-brace-format #, python-brace-format
msgid "{name} hits {opponent}." msgid "{name} hits {opponent}."
@ -103,8 +132,8 @@ msgstr "{name} schlägt {opponent}."
#: squirrelbattle/interfaces.py:465 #: squirrelbattle/interfaces.py:465
#, python-brace-format #, python-brace-format
msgid "{name} takes {amount} damage." msgid "{name} takes {damage} damage."
msgstr "{name} nimmt {amount} Schadenspunkte." msgstr ""
#: squirrelbattle/interfaces.py:467 #: squirrelbattle/interfaces.py:467
#, python-brace-format #, python-brace-format
@ -218,10 +247,6 @@ msgstr "Textur-Packung"
msgid "Language" msgid "Language"
msgstr "Sprache" msgstr "Sprache"
#: squirrelbattle/tests/translations_test.py:62
msgid "player"
msgstr "Spieler"
#: squirrelbattle/tests/translations_test.py:64 #: squirrelbattle/tests/translations_test.py:64
msgid "hedgehog" msgid "hedgehog"
msgstr "Igel" msgstr "Igel"

View File

@ -17,6 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} recibe {amount} daño."
#: squirrelbattle/display/menudisplay.py:160 #: squirrelbattle/display/menudisplay.py:160
msgid "INVENTORY" msgid "INVENTORY"
msgstr "INVENTORIO" msgstr "INVENTORIO"
@ -25,11 +29,32 @@ msgstr "INVENTORIO"
msgid "STALL" msgid "STALL"
msgstr "PUESTO" msgstr "PUESTO"
#: squirrelbattle/display/statsdisplay.py:36 #: squirrelbattle/display/statsdisplay.py:23
#: squirrelbattle/tests/translations_test.py:60
msgid "player"
msgstr "jugador"
#: squirrelbattle/display/statsdisplay.py:35
msgid "Inventory:" msgid "Inventory:"
msgstr "Inventorio :" msgstr "Inventorio :"
#: squirrelbattle/display/statsdisplay.py:55 #: 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" msgid "YOU ARE DEAD"
msgstr "ERES MUERTO" msgstr "ERES MUERTO"
@ -48,11 +73,11 @@ msgstr "El sol está caliente hoy"
#. The bomb is exploding. #. The bomb is exploding.
#. Each entity that is close to the bomb takes damages. #. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed. #. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:151 #: squirrelbattle/entities/items.py:163
msgid "Bomb is exploding." msgid "Bomb is exploding."
msgstr "La bomba está explotando." msgstr "La bomba está explotando."
#: squirrelbattle/entities/items.py:248 #: squirrelbattle/entities/items.py:344
#, python-brace-format #, python-brace-format
msgid "{player} exchanged its body with {entity}." msgid "{player} exchanged its body with {entity}."
msgstr "{player} intercambió su cuerpo con {entity}." msgstr "{player} intercambió su cuerpo con {entity}."
@ -95,6 +120,10 @@ msgstr ""
"El JSON archivo no es correcto.\n" "El JSON archivo no es correcto.\n"
"Su guarda parece corrupta. Fue eliminada." "Su guarda parece corrupta. Fue eliminada."
#: squirrelbattle/interfaces.py:452
msgid "It's a critical hit!"
msgstr ""
#: squirrelbattle/interfaces.py:453 #: squirrelbattle/interfaces.py:453
#, python-brace-format #, python-brace-format
msgid "{name} hits {opponent}." msgid "{name} hits {opponent}."
@ -102,8 +131,8 @@ msgstr "{name} golpea a {opponent}."
#: squirrelbattle/interfaces.py:465 #: squirrelbattle/interfaces.py:465
#, python-brace-format #, python-brace-format
msgid "{name} takes {amount} damage." msgid "{name} takes {damage} damage."
msgstr "{name} recibe {amount} daño." msgstr ""
#: squirrelbattle/interfaces.py:467 #: squirrelbattle/interfaces.py:467
#, python-brace-format #, python-brace-format
@ -217,10 +246,6 @@ msgstr "Paquete de texturas"
msgid "Language" msgid "Language"
msgstr "Languaje" msgstr "Languaje"
#: squirrelbattle/tests/translations_test.py:62
msgid "player"
msgstr "jugador"
#: squirrelbattle/tests/translations_test.py:64 #: squirrelbattle/tests/translations_test.py:64
msgid "hedgehog" msgid "hedgehog"
msgstr "erizo" msgstr "erizo"

View File

@ -17,6 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} prend {amount} points de dégât."
#: squirrelbattle/display/menudisplay.py:160 #: squirrelbattle/display/menudisplay.py:160
msgid "INVENTORY" msgid "INVENTORY"
msgstr "INVENTAIRE" msgstr "INVENTAIRE"
@ -25,11 +29,32 @@ msgstr "INVENTAIRE"
msgid "STALL" msgid "STALL"
msgstr "STAND" msgstr "STAND"
#: squirrelbattle/display/statsdisplay.py:36 #: squirrelbattle/display/statsdisplay.py:23
#: squirrelbattle/tests/translations_test.py:60
msgid "player"
msgstr "joueur"
#: squirrelbattle/display/statsdisplay.py:35
msgid "Inventory:" msgid "Inventory:"
msgstr "Inventaire :" msgstr "Inventaire :"
#: squirrelbattle/display/statsdisplay.py:55 #: 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" msgid "YOU ARE DEAD"
msgstr "VOUS ÊTES MORT" msgstr "VOUS ÊTES MORT"
@ -49,11 +74,11 @@ msgstr "Le soleil est chaud aujourd'hui"
#. The bomb is exploding. #. The bomb is exploding.
#. Each entity that is close to the bomb takes damages. #. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed. #. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:151 #: squirrelbattle/entities/items.py:163
msgid "Bomb is exploding." msgid "Bomb is exploding."
msgstr "La bombe explose." msgstr "La bombe explose."
#: squirrelbattle/entities/items.py:248 #: squirrelbattle/entities/items.py:344
#, python-brace-format #, python-brace-format
msgid "{player} exchanged its body with {entity}." msgid "{player} exchanged its body with {entity}."
msgstr "{player} a échangé son corps avec {entity}." msgstr "{player} a échangé son corps avec {entity}."
@ -96,6 +121,10 @@ msgstr ""
"Le fichier JSON de sauvegarde est incorrect.\n" "Le fichier JSON de sauvegarde est incorrect.\n"
"Votre sauvegarde semble corrompue. Elle a été supprimée." "Votre sauvegarde semble corrompue. Elle a été supprimée."
#: squirrelbattle/interfaces.py:452
msgid "It's a critical hit!"
msgstr "C'est un coup critique !"
#: squirrelbattle/interfaces.py:453 #: squirrelbattle/interfaces.py:453
#, python-brace-format #, python-brace-format
msgid "{name} hits {opponent}." msgid "{name} hits {opponent}."
@ -103,8 +132,8 @@ msgstr "{name} frappe {opponent}."
#: squirrelbattle/interfaces.py:465 #: squirrelbattle/interfaces.py:465
#, python-brace-format #, python-brace-format
msgid "{name} takes {amount} damage." msgid "{name} takes {damage} damage."
msgstr "{name} prend {amount} points de dégât." msgstr "{name} prend {damage} dégâts."
#: squirrelbattle/interfaces.py:467 #: squirrelbattle/interfaces.py:467
#, python-brace-format #, python-brace-format
@ -218,10 +247,6 @@ msgstr "Pack de textures"
msgid "Language" msgid "Language"
msgstr "Langue" msgstr "Langue"
#: squirrelbattle/tests/translations_test.py:62
msgid "player"
msgstr "joueur"
#: squirrelbattle/tests/translations_test.py:64 #: squirrelbattle/tests/translations_test.py:64
msgid "hedgehog" msgid "hedgehog"
msgstr "hérisson" msgstr "hérisson"
@ -256,7 +281,7 @@ msgstr "bombe"
#: squirrelbattle/tests/translations_test.py:73 #: squirrelbattle/tests/translations_test.py:73
msgid "explosion" msgid "explosion"
msgstr "" msgstr "explosion"
#: squirrelbattle/tests/translations_test.py:74 #: squirrelbattle/tests/translations_test.py:74
msgid "heart" msgid "heart"

View File

@ -19,6 +19,7 @@ class TestEntities(unittest.TestCase):
""" """
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt")) self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.player = Player() self.player = Player()
self.player.constitution = 0
self.map.add_entity(self.player) self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x) self.player.move(self.map.start_y, self.map.start_x)
@ -55,6 +56,7 @@ class TestEntities(unittest.TestCase):
self.assertTrue(entity.dead) self.assertTrue(entity.dead)
entity = Rabbit() entity = Rabbit()
entity.critical = 0
self.map.add_entity(entity) self.map.add_entity(entity)
entity.move(15, 44) entity.move(15, 44)
# Move randomly # Move randomly
@ -76,6 +78,7 @@ class TestEntities(unittest.TestCase):
{self.player.name.capitalize()} takes {entity.strength} damage.") {self.player.name.capitalize()} takes {entity.strength} damage.")
# Fight the rabbit # Fight the rabbit
self.player.critical = 0
old_health = entity.health old_health = entity.health
self.player.move_down() self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength) self.assertEqual(entity.health, old_health - self.player.strength)

View File

@ -2,13 +2,16 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os import os
import random
import unittest import unittest
from ..bootstrap import Bootstrap from ..bootstrap import Bootstrap
from ..display.display import Display from ..display.display import Display
from ..display.display_manager import DisplayManager from ..display.display_manager import DisplayManager
from ..entities.friendly import Merchant, Sunflower 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 ..entities.player import Player
from ..enums import DisplayActions from ..enums import DisplayActions
from ..game import Game, KeyValues, GameMode from ..game import Game, KeyValues, GameMode
@ -613,6 +616,100 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.SPACE) self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.PLAY) 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: def test_ladders(self) -> None:
""" """
Ensure that the player can climb on ladders. Ensure that the player can climb on ladders.