Merge branch 'levelup' into 'master'

Levelup

Closes #69

See merge request ynerant/squirrel-battle!65
This commit is contained in:
eichhornchen 2021-01-10 18:08:27 +01:00
commit 0de2df0bd2
12 changed files with 133 additions and 21 deletions

View File

@ -57,6 +57,7 @@ There are several special control keys, they can be changed in the settings menu
* To drop an object from the inventory, use r (to pick up an object, simply go on its tile, its automatic)
* To talk to certains entities (or open a chest), use t and then select the direction of the entity
* To wait a turn (rather than moving), use w
* To dance and confuse the ennemies, use y
* To use a ladder, use <
The dungeon consists in empty tiles (you can not go there), walls (which you can not cross) and floor ( :) ). Entities that move are usually monsters, but if you see a trumpet (or a '/'), do not kill it ! It is a familiar that will help you defeat monsters. Entities that do not move are either entities to which you can talk, like merchants and ... chests for some reason, or objects. Differentiating the two is not difficult, trying to go on the same tile as a living entity (or a chest) is impossible. Objects have pretty clear names, so it should not be too difficult determining what they do (if you still don't know, you can either read the docs, or test for yourself (beware of surprises though))

View File

@ -27,6 +27,9 @@ Les touches utilisées de base sont :
* **Lacher un objet** : r
* **Parler** : t
* **Attendre** : w
* **Utiliser une arme à distance** : l
* **Dancer** : y
* **Utiliser une échelle** : <
Autres
------

View File

@ -3,7 +3,7 @@
from random import choice, shuffle
from .items import Item
from .items import Bomb, Item
from .monsters import Monster
from .player import Player
from ..interfaces import Entity, FightingEntity, FriendlyEntity, \
@ -48,11 +48,14 @@ 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 or [])
self.annihilated = False
if not self.inventory:
for i in range(3):
self.inventory.append(choice(Item.get_all_items())())
@ -68,6 +71,10 @@ class Chest(InventoryHolder, FriendlyEntity):
"""
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
@ -75,14 +82,14 @@ class Chest(InventoryHolder, FriendlyEntity):
"""
Chest can not die
"""
return False
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)
@ -162,6 +169,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

@ -498,7 +498,7 @@ class ScrollofDamage(Item):
class ScrollofWeakening(Item):
"""
A scroll that, when used, reduces the damage of the ennemies for 3 turn.
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):

View File

@ -76,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)
@ -97,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-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 2 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,9 +96,19 @@ 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
# TODO Remove it, that's only fun
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
# TODO Remove it, that's only for fun
self.map.spawn_random_entities(randint(3 * self.level,
10 * self.level))

View File

@ -50,6 +50,7 @@ class KeyValues(Enum):
WAIT = auto()
LADDER = auto()
LAUNCH = auto()
DANCE = auto()
@staticmethod
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
@ -88,4 +89,6 @@ class KeyValues(Enum):
return KeyValues.LADDER
elif key == settings.KEY_LAUNCH:
return KeyValues.LAUNCH
elif key == settings.KEY_DANCE:
return KeyValues.DANCE
return None

View File

@ -179,6 +179,9 @@ class Game:
self.map.tick(self.player)
elif key == KeyValues.LADDER:
self.handle_ladder()
elif key == KeyValues.DANCE:
self.player.dance()
self.map.tick(self.player)
def handle_ladder(self) -> None:
"""

View File

@ -628,8 +628,9 @@ class Entity:
Rabbit, TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet, Chest
return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,
Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet, Chest]
return [BodySnatchPotion, Bomb, Chest, GiantSeaEagle, Heart,
Hedgehog, Merchant, Rabbit, Sunflower, TeddyBear, Tiger,
Trumpet]
@staticmethod
def get_weights() -> list:
@ -637,7 +638,7 @@ class Entity:
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, 3, 1, 2, 4]
return [30, 80, 50, 1, 100, 100, 60, 70, 70, 20, 40, 40]
@staticmethod
def get_all_entity_classes_in_a_dict() -> dict:
@ -706,6 +707,7 @@ class FightingEntity(Entity):
constitution: int
level: int
critical: int
confused: int # Seulement 0 ou 1
def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
strength: int = 0, intelligence: int = 0, charisma: int = 0,
@ -722,6 +724,7 @@ class FightingEntity(Entity):
self.level = level
self.critical = critical
self.effects = [] # effects = temporary buff or weakening of the stats.
self.confused = 0
@property
def dead(self) -> bool:
@ -749,6 +752,10 @@ class FightingEntity(Entity):
The entity deals damage to the opponent
based on their respective stats.
"""
if self.confused:
return _("{name} is confused, it can not hit {opponent}.")\
.format(name=_(self.translated_name.capitalize()),
opponent=_(opponent.translated_name))
diceroll = randint(1, 100)
damage = max(0, self.strength)
string = " "
@ -765,7 +772,7 @@ class FightingEntity(Entity):
The entity takes damage from the attacker
based on their respective stats.
"""
damage = max(0, amount - self.constitution)
damage = max(1, amount - self.constitution)
self.health -= damage
if self.health <= 0:
self.die()

View File

@ -36,6 +36,7 @@ class Settings:
self.KEY_WAIT = ['w', 'Key used to wait']
self.KEY_LADDER = ['<', 'Key used to use ladders']
self.KEY_LAUNCH = ['l', 'Key used to use a bow']
self.KEY_DANCE = ['y', 'Key used to dance']
self.TEXTURE_PACK = ['ascii', 'Texture pack']
self.LOCALE = [locale.getlocale()[0][:2], 'Language']

View File

@ -4,7 +4,7 @@
import random
import unittest
from ..entities.friendly import Trumpet
from ..entities.friendly import Chest, Trumpet
from ..entities.items import BodySnatchPotion, Bomb, Explosion, Heart, Item
from ..entities.monsters import GiantSeaEagle, Hedgehog, Rabbit, \
TeddyBear, Tiger
@ -45,18 +45,19 @@ class TestEntities(unittest.TestCase):
"""
entity = Tiger()
self.map.add_entity(entity)
self.assertEqual(entity.maxhealth, 20)
self.assertEqual(entity.maxhealth, 30)
self.assertEqual(entity.maxhealth, entity.health)
self.assertEqual(entity.strength, 2)
for _ in range(9):
self.assertEqual(entity.strength, 5)
for _ in range(5):
self.assertEqual(entity.hit(entity),
"Tiger hits tiger. Tiger takes 2 damage.")
"Tiger hits tiger. Tiger takes 5 damage.")
self.assertFalse(entity.dead)
self.assertEqual(entity.hit(entity), "Tiger hits tiger. "
+ "Tiger takes 2 damage. Tiger dies.")
+ "Tiger takes 5 damage. Tiger dies.")
self.assertTrue(entity.dead)
entity = Rabbit()
entity.health = 15
entity.critical = 0
self.map.add_entity(entity)
entity.move(15, 44)
@ -94,7 +95,20 @@ class TestEntities(unittest.TestCase):
self.assertTrue(entity.dead)
self.assertGreaterEqual(self.player.current_xp, 3)
# Test the familiars
# Test that a chest is destroyed by a bomb
bomb = Bomb()
bomb.owner = self.player
bomb.move(3, 6)
self.map.add_entity(bomb)
chest = Chest()
chest.move(4, 6)
self.map.add_entity(chest)
bomb.exploding = True
for _ in range(5):
self.map.tick(self.player)
self.assertTrue(chest.annihilated)
def test_familiar(self) -> None:
fam = Trumpet()
entity = Rabbit()
self.map.add_entity(entity)
@ -266,6 +280,15 @@ class TestEntities(unittest.TestCase):
player_state = player.save_state()
self.assertEqual(player_state["current_xp"], 10)
player = Player()
player.map = self.map
player.add_xp(700)
for _ in range(13):
player.level_up()
self.assertEqual(player.level, 12)
self.assertEqual(player.critical, 5 + 95 // 30)
self.assertEqual(player.charisma, 3)
def test_critical_hit(self) -> None:
"""
Ensure that critical hits are working.

View File

@ -160,6 +160,9 @@ class TestGame(unittest.TestCase):
KeyValues.SPACE)
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
None)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DANCE, self.game.settings),
KeyValues.DANCE)
def test_key_press(self) -> None:
"""
@ -249,6 +252,30 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.WAIT)
self.assertNotIn(explosion, self.game.map.entities)
rabbit = Rabbit()
self.game.map.add_entity(rabbit)
self.game.player.move(1, 6)
rabbit.move(3, 6)
self.game.player.charisma = 11
self.game.handle_key_pressed(KeyValues.DANCE)
self.assertEqual(rabbit.confused, 1)
string = rabbit.hit(self.game.player)
self.assertEqual(string,
"{name} is confused, it can not hit {opponent}."
.format(name=_(rabbit.translated_name.capitalize()
), opponent=_(
self.game.player.translated_name
)))
rabbit.confused = 0
self.game.player.charisma = 0
self.game.handle_key_pressed(KeyValues.DANCE)
self.assertEqual(rabbit.confused, 0)
rabbit.die()
self.game.player.charisma = 11
self.game.handle_key_pressed(KeyValues.DANCE)
self.game.player.charisma = 1
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
@ -350,7 +377,7 @@ class TestGame(unittest.TestCase):
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
# Navigate to "texture pack"
for ignored in range(13):
for ignored in range(14):
self.game.handle_key_pressed(KeyValues.DOWN)
# Change texture pack