From 841c7b9f90dc19b75870d8b3a1b0016a99d3643a Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sun, 10 Jan 2021 16:31:16 +0100 Subject: [PATCH 1/5] Chest can be destroyed by bombs. --- squirrelbattle/entities/friendly.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/entities/friendly.py b/squirrelbattle/entities/friendly.py index 36f9db3..ee4f7cd 100644 --- a/squirrelbattle/entities/friendly.py +++ b/squirrelbattle/entities/friendly.py @@ -3,7 +3,7 @@ from random import choice, shuffle -from .items import Item +from .items import Item, Bomb from .monsters import Monster from .player import Player from ..interfaces import Entity, FightingEntity, FriendlyEntity, \ @@ -68,6 +68,9 @@ class Chest(InventoryHolder, FriendlyEntity): """ A chest is not living, it can not take damage """ + if isinstance(attacker, Bomb) : + self.die() + return _("The chest exploded") return _("It's not really effective") @property @@ -82,7 +85,7 @@ 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 +165,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) From dfb591d410eff4c0e126aef58c700440c6619109 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sun, 10 Jan 2021 16:31:46 +0100 Subject: [PATCH 2/5] The player's stats now get better when levelling up. The strength level and frequency of appearance of entities have been changed to offer bettter game experience. --- squirrelbattle/entities/monsters.py | 6 +++--- squirrelbattle/entities/player.py | 13 ++++++++++++- squirrelbattle/interfaces.py | 9 +++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/squirrelbattle/entities/monsters.py b/squirrelbattle/entities/monsters.py index c654428..8f3c2b5 100644 --- a/squirrelbattle/entities/monsters.py +++ b/squirrelbattle/entities/monsters.py @@ -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, diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 8257f85..5b788b2 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -3,6 +3,7 @@ from random import randint from typing import Dict, Optional, Tuple +from math import log from .items import Item from ..interfaces import FightingEntity, InventoryHolder @@ -69,9 +70,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)) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 2280909..60a7c82 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -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: @@ -765,7 +766,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() From 3d48c43886223503587407e0b857d85816e113f2 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sun, 10 Jan 2021 17:10:00 +0100 Subject: [PATCH 3/5] Player can now dance! Closes #69. --- squirrelbattle/entities/items.py | 2 +- squirrelbattle/entities/player.py | 27 +++++++++++++++++++++++++++ squirrelbattle/enums.py | 3 +++ squirrelbattle/game.py | 4 ++++ squirrelbattle/interfaces.py | 6 ++++++ squirrelbattle/settings.py | 1 + 6 files changed, 42 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 94a9e36..ac502ea 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -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): diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 5b788b2..b3cb986 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -7,6 +7,7 @@ from math import log from .items import Item from ..interfaces import FightingEntity, InventoryHolder +from ..translations import gettext as _, Translator class Player(InventoryHolder, FightingEntity): @@ -62,6 +63,32 @@ 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. diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index b6b4bcd..42bd643 100644 --- a/squirrelbattle/enums.py +++ b/squirrelbattle/enums.py @@ -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 diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 56ab6ed..73d45eb 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -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: """ @@ -291,6 +294,7 @@ class Game: if self.player.equipped_main: self.player.equipped_main.throw(direction) + def handle_key_pressed_inventory(self, key: KeyValues) -> None: """ In the inventory menu, we can interact with items or close the menu. diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 60a7c82..3832cef 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -707,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, @@ -723,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: @@ -750,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 = " " diff --git a/squirrelbattle/settings.py b/squirrelbattle/settings.py index 92a8b37..3fd27c5 100644 --- a/squirrelbattle/settings.py +++ b/squirrelbattle/settings.py @@ -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'] From afaa12d86bb7e9c612481944c782bd467375bf09 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sun, 10 Jan 2021 17:13:07 +0100 Subject: [PATCH 4/5] Updated doc and README conderning dancing --- README.md | 1 + docs/settings.rst | 3 +++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 4c1356c..ae7be80 100644 --- a/README.md +++ b/README.md @@ -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)) diff --git a/docs/settings.rst b/docs/settings.rst index 60fa5c1..de17fca 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -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 ------ From 93e51d9240940011f2d31d8971ace581b189215e Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sun, 10 Jan 2021 18:04:33 +0100 Subject: [PATCH 5/5] Testing + linting (yes there remains two linting errors, i don't know what to do. --- squirrelbattle/entities/friendly.py | 10 +++++--- squirrelbattle/entities/player.py | 23 ++++++++--------- squirrelbattle/game.py | 1 - squirrelbattle/interfaces.py | 6 ++--- squirrelbattle/tests/entities_test.py | 37 ++++++++++++++++++++++----- squirrelbattle/tests/game_test.py | 29 ++++++++++++++++++++- 6 files changed, 79 insertions(+), 27 deletions(-) diff --git a/squirrelbattle/entities/friendly.py b/squirrelbattle/entities/friendly.py index ee4f7cd..57506e9 100644 --- a/squirrelbattle/entities/friendly.py +++ b/squirrelbattle/entities/friendly.py @@ -3,7 +3,7 @@ from random import choice, shuffle -from .items import Item, Bomb +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,8 +71,9 @@ class Chest(InventoryHolder, FriendlyEntity): """ A chest is not living, it can not take damage """ - if isinstance(attacker, Bomb) : + if isinstance(attacker, Bomb): self.die() + self.annihilated = True return _("The chest exploded") return _("It's not really effective") @@ -78,7 +82,7 @@ class Chest(InventoryHolder, FriendlyEntity): """ Chest can not die """ - return False + return self.annihilated class Sunflower(FriendlyEntity): diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index b3cb986..7648639 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -1,13 +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 math import log from .items import Item from ..interfaces import FightingEntity, InventoryHolder -from ..translations import gettext as _, Translator +from ..translations import gettext as _ class Player(InventoryHolder, FightingEntity): @@ -74,10 +74,10 @@ class Player(InventoryHolder, FightingEntity): 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]) + 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.")) @@ -88,7 +88,6 @@ class Player(InventoryHolder, FightingEntity): self.map.logs.add_message( _("The dance was not effective...")) - def level_up(self) -> None: """ Add as many levels as possible to the player. @@ -97,18 +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.maxhealth += int(2 * log(self.level) / log(2)) self.health = self.maxhealth self.strength = self.strength + 1 - if self.level % 3 == 0 : + if self.level % 3 == 0: self.dexterity += 1 self.constitution += 1 - if self.level % 4 == 0 : + if self.level % 4 == 0: self.intelligence += 1 - if self.level % 6 == 0 : + if self.level % 6 == 0: self.charisma += 1 if self.level % 10 == 0 and self.critical < 95: - self.critical += (100-self.charisma)//30 + 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)) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 73d45eb..bde825f 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -294,7 +294,6 @@ class Game: if self.player.equipped_main: self.player.equipped_main.throw(direction) - def handle_key_pressed_inventory(self, key: KeyValues) -> None: """ In the inventory menu, we can interact with items or close the menu. diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 3832cef..bf8ddbe 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -629,7 +629,7 @@ class Entity: from squirrelbattle.entities.friendly import Merchant, Sunflower, \ Trumpet, Chest return [BodySnatchPotion, Bomb, Chest, GiantSeaEagle, Heart, - Hedgehog, Merchant, Rabbit, Sunflower, TeddyBear, Tiger, + Hedgehog, Merchant, Rabbit, Sunflower, TeddyBear, Tiger, Trumpet] @staticmethod @@ -754,8 +754,8 @@ class FightingEntity(Entity): """ if self.confused: return _("{name} is confused, it can not hit {opponent}.")\ - .format(name=_(self.translated_name.capitalize()), - opponent=_(opponent.translated_name)) + .format(name=_(self.translated_name.capitalize()), + opponent=_(opponent.translated_name)) diceroll = randint(1, 100) damage = max(0, self.strength) string = " " diff --git a/squirrelbattle/tests/entities_test.py b/squirrelbattle/tests/entities_test.py index db32877..a0e2548 100644 --- a/squirrelbattle/tests/entities_test.py +++ b/squirrelbattle/tests/entities_test.py @@ -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. diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index b9de160..a3c3a87 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -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