Merge branch 'master' into map_generation

# Conflicts:
#	squirrelbattle/entities/player.py
#	squirrelbattle/game.py
#	squirrelbattle/interfaces.py
#	squirrelbattle/tests/game_test.py
This commit is contained in:
Yohann D'ANELLO
2021-01-10 22:16:11 +01:00
54 changed files with 1909 additions and 934 deletions

View File

@ -1,2 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,10 +1,15 @@
from ..interfaces import FriendlyEntity, InventoryHolder, Map, FightingEntity
from ..translations import gettext as _
from .player import Player
from .monsters import Monster
from .items import Item
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import choice, shuffle
from .items import Bomb, Item
from .monsters import Monster
from .player import Player
from ..interfaces import Entity, FightingEntity, FriendlyEntity, \
InventoryHolder, Map
from ..translations import gettext as _
class Merchant(InventoryHolder, FriendlyEntity):
"""
@ -17,11 +22,13 @@ class Merchant(InventoryHolder, FriendlyEntity):
return super().keys() + ["inventory", "hazel"]
def __init__(self, name: str = "merchant", inventory: list = None,
hazel: int = 75, *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
self.inventory = self.translate_inventory(inventory or [])
hazel: int = 75, maxhealth: int = 8, *args, **kwargs):
super().__init__(name=name, maxhealth=maxhealth, *args, **kwargs)
self.inventory = self.translate_inventory(inventory) \
if inventory is not None else None
self.hazel = hazel
if not self.inventory:
if self.inventory is None:
self.inventory = []
for i in range(5):
self.inventory.append(choice(Item.get_all_items())())
@ -39,11 +46,54 @@ class Merchant(InventoryHolder, FriendlyEntity):
self.hazel += hz
class Chest(InventoryHolder, FriendlyEntity):
"""
A class of chest inanimate entities which contain objects.
"""
annihilated: bool
def __init__(self, name: str = "chest", inventory: list = None,
hazel: int = 0, *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
self.hazel = hazel
self.inventory = self.translate_inventory(inventory) \
if inventory is not None else None
self.annihilated = False
if self.inventory is None:
self.inventory = []
for i in range(3):
self.inventory.append(choice(Item.get_all_items())())
def talk_to(self, player: Player) -> str:
"""
This function is used to open the chest's inventory in a menu,
and allows the player to take objects.
"""
return _("You have opened the chest")
def take_damage(self, attacker: Entity, amount: int) -> str:
"""
A chest is not living, it can not take damage
"""
if isinstance(attacker, Bomb):
self.die()
self.annihilated = True
return _("The chest exploded")
return _("It's not really effective")
@property
def dead(self) -> bool:
"""
Chest can not die
"""
return self.annihilated
class Sunflower(FriendlyEntity):
"""
A friendly sunflower.
"""
def __init__(self, maxhealth: int = 15,
def __init__(self, maxhealth: int = 20,
*args, **kwargs) -> None:
super().__init__(name="sunflower", maxhealth=maxhealth, *args, **kwargs)
@ -123,6 +173,6 @@ class Trumpet(Familiar):
A class of familiars.
"""
def __init__(self, name: str = "trumpet", strength: int = 3,
maxhealth: int = 20, *args, **kwargs) -> None:
maxhealth: int = 30, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -1,10 +1,10 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import choice, randint
from typing import Optional
from typing import Any, Optional
from ..interfaces import Entity, FightingEntity, Map, InventoryHolder
from ..interfaces import Entity, FightingEntity, InventoryHolder, Map
from ..translations import gettext as _
@ -47,6 +47,11 @@ class Item(Entity):
Indicates what should be done when the item is used.
"""
def throw(self, direction: int) -> Any:
"""
Indicates what should be done when the item is thrown.
"""
def equip(self) -> None:
"""
Indicates what should be done when the item is equipped.
@ -86,16 +91,22 @@ class Item(Entity):
"""
Returns the list of all item classes.
"""
return [BodySnatchPotion, Chestplate, Bomb, Heart, Helmet, Monocle,
Shield, Sword, RingCritical, RingXP]
return [BodySnatchPotion, Bomb, Bow, Chestplate, FireBallStaff,
Heart, Helmet, Monocle, ScrollofDamage, ScrollofWeakening,
Shield, Sword, RingCritical, RingXP, Ruler]
def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool:
def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder,
for_free: bool = False) -> bool:
"""
Does all necessary actions when an object is to be sold.
Is overwritten by some classes that cannot exist in the player's
inventory.
"""
if buyer.hazel >= self.price:
if for_free:
self.hold(buyer)
seller.remove_from_inventory(self)
return True
elif buyer.hazel >= self.price:
self.hold(buyer)
seller.remove_from_inventory(self)
buyer.change_hazel_balance(-self.price)
@ -266,6 +277,15 @@ class Sword(Weapon):
super().__init__(name=name, price=price, *args, **kwargs)
class Ruler(Weapon):
"""
A basic weapon
"""
def __init__(self, name: str = "ruler", price: int = 2,
damage: int = 1, *args, **kwargs):
super().__init__(name=name, price=price, damage=damage, *args, **kwargs)
class Armor(Item):
"""
Class of items that increase the player's constitution.
@ -455,6 +475,166 @@ class RingXP(Ring):
*args, **kwargs)
class ScrollofDamage(Item):
"""
A scroll that, when used, deals damage to all entities in a certain radius.
"""
def __init__(self, name: str = "scroll_of_damage", price: int = 18,
*args, **kwargs):
super().__init__(name=name, price=price, *args, **kwargs)
def use(self) -> None:
"""
Find all entities within a radius of 5, and deal damage based on the
player's intelligence.
"""
for entity in self.held_by.map.entities:
if entity.is_fighting_entity() and not entity == self.held_by:
if entity.distance(self.held_by) <= 5:
self.held_by.map.logs.add_message(entity.take_damage(
self.held_by, self.held_by.intelligence))
self.held_by.inventory.remove(self)
class ScrollofWeakening(Item):
"""
A scroll that, when used, reduces the damage of the ennemies for 3 turns.
"""
def __init__(self, name: str = "scroll_of_weakening", price: int = 13,
*args, **kwargs):
super().__init__(name=name, price=price, *args, **kwargs)
def use(self) -> None:
"""
Find all entities and reduce their damage.
"""
for entity in self.held_by.map.entities:
if entity.is_fighting_entity() and not entity == self.held_by:
entity.strength = entity.strength - \
max(1, self.held_by.intelligence // 2)
entity.effects.append(["strength",
-max(1, self.held_by.intelligence // 2),
3])
self.held_by.map.logs.add_message(
_(f"The ennemies have -{max(1, self.held_by.intelligence // 2)}"
+ "strength for 3 turns"))
self.held_by.inventory.remove(self)
class LongRangeWeapon(Weapon):
def __init__(self, damage: int = 4,
rang: int = 3, *args, **kwargs):
super().__init__(*args, **kwargs)
self.damage = damage
self.range = rang
def throw(self, direction: int) -> Any:
to_kill = None
for entity in self.held_by.map.entities:
if entity.is_fighting_entity():
if direction == 0 and self.held_by.x == entity.x \
and self.held_by.y - entity.y > 0 and \
self.held_by.y - entity.y <= self.range:
to_kill = entity
elif direction == 2 and self.held_by.x == entity.x \
and entity.y - self.held_by.y > 0 and \
entity.y - self.held_by.y <= self.range:
to_kill = entity
elif direction == 1 and self.held_by.y == entity.y \
and entity.x - self.held_by.x > 0 and \
entity.x - self.held_by.x <= self.range:
to_kill = entity
elif direction == 3 and self.held_by.y == entity.y \
and self.held_by.x - entity.x > 0 and \
self.held_by.x - entity.x <= self.range:
to_kill = entity
if to_kill:
line = _("{name}").format(name=to_kill.translated_name.capitalize()
) + self.string + " "\
+ to_kill.take_damage(
self.held_by, self.damage
+ getattr(self.held_by, self.stat))
self.held_by.map.logs.add_message(line)
return (to_kill.y, to_kill.x) if to_kill else None
def equip(self) -> None:
"""
Equip the weapon.
"""
self.held_by.remove_from_inventory(self)
self.held_by.equipped_main = self
@property
def stat(self) -> str:
"""
The stat that is used when using the object: dexterity for a bow
or intelligence for a magic staff.
"""
@property
def string(self) -> str:
"""
The string that is printed when we hit an ennemy.
"""
class Bow(LongRangeWeapon):
"""
A type of long range weapon that deals damage
based on the player's dexterity
"""
def __init__(self, name: str = "bow", price: int = 22, damage: int = 4,
rang: int = 3, *args, **kwargs):
super().__init__(name=name, price=price, damage=damage,
rang=rang, *args, **kwargs)
@property
def stat(self) -> str:
"""
Here it is dexterity
"""
return "dexterity"
@property
def string(self) -> str:
return _(" is shot by an arrow.")
class FireBallStaff(LongRangeWeapon):
"""
A type of powerful long range weapon that deals damage
based on the player's intelligence
"""
def __init__(self, name: str = "fire_ball_staff", price: int = 36,
damage: int = 6, rang: int = 4, *args, **kwargs):
super().__init__(name=name, price=price, damage=damage,
rang=rang, *args, **kwargs)
@property
def stat(self) -> str:
"""
Here it is intelligence
"""
return "intelligence"
@property
def string(self) -> str:
return _(" is shot by a fire ball.")
def throw(self, direction: int) -> Any:
"""
Adds an explosion animation when killing something.
"""
coord = super().throw(direction)
if coord:
y = coord[0]
x = coord[1]
explosion = Explosion(y=y, x=x)
self.held_by.map.add_entity(explosion)
return y, x
class Monocle(Item):
def __init__(self, name: str = "monocle", price: int = 10,
*args, **kwargs):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import shuffle
@ -31,6 +31,7 @@ class Monster(FightingEntity):
By default, a monster will move randomly where it is possible
If the player is closeby, the monster runs to the player.
"""
super().act(m)
target = None
for entity in m.entities:
if self.distance_squared(entity) <= 25 and \
@ -42,7 +43,9 @@ class Monster(FightingEntity):
# that targets the player.
# If they can not move and are already close to the player,
# they hit.
if target and (self.y, self.x) in target.paths:
if target and (self.y, self.x) in target.paths and \
self.map.is_visible_from(self.y, self.x,
target.y, target.x, 5):
# Moves to target player by choosing the best available path
for next_y, next_x in target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
@ -73,8 +76,8 @@ class Tiger(Monster):
"""
A tiger monster.
"""
def __init__(self, name: str = "tiger", strength: int = 2,
maxhealth: int = 20, *args, **kwargs) -> None:
def __init__(self, name: str = "tiger", strength: int = 5,
maxhealth: int = 30, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
@ -94,7 +97,7 @@ class Rabbit(Monster):
A rabbit monster.
"""
def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, critical: int = 30,
maxhealth: int = 20, critical: int = 30,
*args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, critical=critical,

View File

@ -1,11 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from math import log
from random import randint
from typing import Dict, Optional, Tuple
from .items import Item
from ..interfaces import FightingEntity, InventoryHolder
from ..translations import gettext as _
class Player(InventoryHolder, FightingEntity):
@ -61,6 +63,31 @@ class Player(InventoryHolder, FightingEntity):
self.recalculate_paths()
self.map.compute_visibility(self.y, self.x, self.vision)
def dance(self) -> None:
"""
Dancing has a certain probability or making ennemies unable
to fight for 3 turns. That probability depends on the player's
charisma.
"""
diceroll = randint(1, 10)
found = False
if diceroll <= self.charisma:
for entity in self.map.entities:
if entity.is_fighting_entity() and not entity == self \
and entity.distance(self) <= 3:
found = True
entity.confused = 1
entity.effects.append(["confused", 1, 3])
if found:
self.map.logs.add_message(_(
"It worked! Nearby ennemies will be confused for 3 turns."))
else:
self.map.logs.add_message(_(
"It worked, but there is no one nearby..."))
else:
self.map.logs.add_message(
_("The dance was not effective..."))
def level_up(self) -> None:
"""
Add as many levels as possible to the player.
@ -69,8 +96,18 @@ class Player(InventoryHolder, FightingEntity):
self.level += 1
self.current_xp -= self.max_xp
self.max_xp = self.level * 10
self.maxhealth += int(2 * log(self.level) / log(2))
self.health = self.maxhealth
self.strength = self.strength + 1
if self.level % 3 == 0:
self.dexterity += 1
self.constitution += 1
if self.level % 4 == 0:
self.intelligence += 1
if self.level % 6 == 0:
self.charisma += 1
if self.level % 10 == 0 and self.critical < 95:
self.critical += (100 - self.charisma) // 30
def add_xp(self, xp: int) -> None:
"""