From 5736c2300bf1683fee78bb99b3774fe510165bb3 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 11:54:39 +0100 Subject: [PATCH 01/12] Added a scroll object that deals damage based on the player intelligence. Related to #60 --- squirrelbattle/display/texturepack.py | 3 +++ squirrelbattle/entities/items.py | 21 +++++++++++++++++++++ squirrelbattle/interfaces.py | 4 +++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 81302b3..80a20a3 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -34,6 +34,7 @@ class TexturePack: RABBIT: str RING_OF_CRITICAL_DAMAGE: str RING_OF_MORE_EXPERIENCE: str + SCROLL_OF_DAMAGE: str SHIELD: str SUNFLOWER: str SWORD: str @@ -95,6 +96,7 @@ TexturePack.ASCII_PACK = TexturePack( TIGER='n', TRUMPET='/', WALL='#', + SCROLL_OF_DAMAGE=']', ) TexturePack.SQUIRREL_PACK = TexturePack( @@ -131,4 +133,5 @@ TexturePack.SQUIRREL_PACK = TexturePack( TIGER='๐Ÿ…', TRUMPET='๐ŸŽบ', WALL='๐Ÿงฑ', + SCROLL_OF_DAMAGE='๐Ÿ“œ', ) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 0436e37..6909fc8 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -422,3 +422,24 @@ class RingXP(Ring): experience: float = 2, *args, **kwargs): super().__init__(name=name, price=price, experience=experience, *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) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index f1f740b..1dff831 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -629,7 +629,8 @@ class Entity: from squirrelbattle.entities.friendly import Merchant, Sunflower, \ Trumpet from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ - Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP + Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \ + ScrollofDamage return { "Tiger": Tiger, "Bomb": Bomb, @@ -649,6 +650,7 @@ class Entity: "Helmet": Helmet, "RingCritical": RingCritical, "RingXP": RingXP, + "ScrollofDamage": ScrollofDamage, } def save_state(self) -> dict: From bde33e9232fafa9b5e2a0828c4708ed3db5df15d Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 16:14:40 +0100 Subject: [PATCH 02/12] Added a second scroll object closes #60 --- squirrelbattle/display/texturepack.py | 3 +++ squirrelbattle/entities/items.py | 25 ++++++++++++++++++++++++- squirrelbattle/entities/monsters.py | 1 + squirrelbattle/interfaces.py | 19 +++++++++++++++++-- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 80a20a3..6ddf461 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -35,6 +35,7 @@ class TexturePack: RING_OF_CRITICAL_DAMAGE: str RING_OF_MORE_EXPERIENCE: str SCROLL_OF_DAMAGE: str + SCROLL_OF_WEAKENING: str SHIELD: str SUNFLOWER: str SWORD: str @@ -97,6 +98,7 @@ TexturePack.ASCII_PACK = TexturePack( TRUMPET='/', WALL='#', SCROLL_OF_DAMAGE=']', + SCROLL_OF_WEAKENING=']', ) TexturePack.SQUIRREL_PACK = TexturePack( @@ -134,4 +136,5 @@ TexturePack.SQUIRREL_PACK = TexturePack( TRUMPET='๐ŸŽบ', WALL='๐Ÿงฑ', SCROLL_OF_DAMAGE='๐Ÿ“œ', + SCROLL_OF_WEAKENING='๐Ÿ“œ', ) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 6909fc8..740c1bf 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -80,7 +80,8 @@ class Item(Entity): Returns the list of all item classes. """ return [BodySnatchPotion, Bomb, Heart, Shield, Sword,\ - Chestplate, Helmet, RingCritical, RingXP] + Chestplate, Helmet, RingCritical, RingXP, \ + ScrollofDamage, ScrollofWeakening] def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: """ @@ -443,3 +444,25 @@ class ScrollofDamage(Item): 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 turn. + """ + + 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) diff --git a/squirrelbattle/entities/monsters.py b/squirrelbattle/entities/monsters.py index e22aa51..cae12f2 100644 --- a/squirrelbattle/entities/monsters.py +++ b/squirrelbattle/entities/monsters.py @@ -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 \ diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 1dff831..814736f 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -630,7 +630,7 @@ class Entity: Trumpet from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \ - ScrollofDamage + ScrollofDamage, ScrollofWeakening return { "Tiger": Tiger, "Bomb": Bomb, @@ -651,6 +651,7 @@ class Entity: "RingCritical": RingCritical, "RingXP": RingXP, "ScrollofDamage": ScrollofDamage, + "ScrollofWeakening": ScrollofWeakening, } def save_state(self) -> dict: @@ -693,6 +694,7 @@ class FightingEntity(Entity): self.constitution = constitution self.level = level self.critical = critical + self.effects = [] #effects are temporary buff or weakening of the stats. @property def dead(self) -> bool: @@ -701,13 +703,26 @@ class FightingEntity(Entity): """ return self.health <= 0 + def act(self, m: Map) -> None: + """ + Refreshes all current effects. + """ + for i in range(len(self.effects)): + self.effects[i][2] -= 1 + + l = self.effects[:] + for i in range(len(l)): + if l[i][2] <= 0: + setattr(self, l[i][0], getattr(self, l[i][0])-l[i][1]) + self.effects.remove(l[i]) + def hit(self, opponent: "FightingEntity") -> str: """ The entity deals damage to the opponent based on their respective stats. """ diceroll = randint(1, 100) - damage = self.strength + damage = max(0, self.strength) string = " " if diceroll <= self.critical: # It is a critical hit damage *= 4 From f6210a6356cd0a42b992395e84143bc56a62625a Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 18:06:26 +0100 Subject: [PATCH 03/12] Added a Bow, related to #64 --- squirrelbattle/display/texturepack.py | 6 +++ squirrelbattle/entities/items.py | 60 +++++++++++++++++++++++++-- squirrelbattle/entities/player.py | 5 ++- squirrelbattle/enums.py | 3 ++ squirrelbattle/game.py | 32 ++++++++++++++ squirrelbattle/interfaces.py | 4 +- squirrelbattle/settings.py | 1 + 7 files changed, 105 insertions(+), 6 deletions(-) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 6ddf461..0ced27f 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -21,6 +21,7 @@ class TexturePack: BODY_SNATCH_POTION: str BOMB: str + BOW: str CHESTPLATE: str EAGLE: str EMPTY: str @@ -34,6 +35,7 @@ class TexturePack: RABBIT: str RING_OF_CRITICAL_DAMAGE: str RING_OF_MORE_EXPERIENCE: str + RULER: str SCROLL_OF_DAMAGE: str SCROLL_OF_WEAKENING: str SHIELD: str @@ -75,6 +77,7 @@ TexturePack.ASCII_PACK = TexturePack( BODY_SNATCH_POTION='S', BOMB='รง', + BOW=')', CHESTPLATE='(', EAGLE='ยต', EMPTY=' ', @@ -90,6 +93,7 @@ TexturePack.ASCII_PACK = TexturePack( RABBIT='Y', RING_OF_CRITICAL_DAMAGE='o', RING_OF_MORE_EXPERIENCE='o', + RULER='\\', SHIELD='D', SUNFLOWER='I', SWORD='\u2020', @@ -112,6 +116,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( BODY_SNATCH_POTION='๐Ÿ”€', BOMB='๐Ÿ’ฃ', + BOW='๐Ÿน', CHESTPLATE='๐Ÿฆบ', EAGLE='๐Ÿฆ…', EMPTY=' ', @@ -128,6 +133,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( RABBIT='๐Ÿ‡', RING_OF_CRITICAL_DAMAGE='๐Ÿ’', RING_OF_MORE_EXPERIENCE='๐Ÿ’', + RULER='๐Ÿ“', SHIELD='๐Ÿ›ก๏ธ ', SUNFLOWER='๐ŸŒป', SWORD='๐Ÿ—ก๏ธ ', diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 740c1bf..b223688 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -40,6 +40,11 @@ class Item(Entity): Indicates what should be done when the item is used. """ + def throw(self, direction: int) -> None: + """ + Indicates what should be done when the item is thrown. + """ + def equip(self) -> None: """ Indicates what should be done when the item is equipped. @@ -81,7 +86,7 @@ class Item(Entity): """ return [BodySnatchPotion, Bomb, Heart, Shield, Sword,\ Chestplate, Helmet, RingCritical, RingXP, \ - ScrollofDamage, ScrollofWeakening] + ScrollofDamage, ScrollofWeakening, Ruler, Bow] def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: """ @@ -251,6 +256,13 @@ class Sword(Weapon): *args, **kwargs): 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): """ @@ -428,7 +440,6 @@ 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) @@ -449,7 +460,6 @@ class ScrollofWeakening(Item): """ A scroll that, when used, reduces the damage of the ennemies for 3 turn. """ - def __init__(self, name: str = "scroll_of_weakening", price: int = 13, *args, **kwargs): super().__init__(name=name, price=price, *args, **kwargs) @@ -459,10 +469,52 @@ class ScrollofWeakening(Item): 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: + 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 Bow(Item): + """ + A type of throwable weapon that deals damage based on the player's dexterity. + """ + def __init__(self, name: str = "bow", price: int = 22, damage = 4, + rang = 3, *args, **kwargs): + super().__init__(name=name, price=price, *args, **kwargs) + self.damage = damage + self.range = rang + + def throw(self, direction: int) -> str: + 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: + self.held_by.map.logs.add_message(_("{name} is shot by an arrow.")\ + .format(name=to_kill.translated_name.capitalize())+ " " \ + + to_kill.take_damage(self.held_by, self.damage + self.held_by.dexterity)) + + def equip(self) -> None: + """ + Equip the bow. + """ + self.held_by.remove_from_inventory(self) + self.held_by.equipped_main = self diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 615dfd5..8981341 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -4,7 +4,7 @@ from random import randint from typing import Dict, Optional, Tuple -from .items import Item +from .items import Item, Bow from ..interfaces import FightingEntity, InventoryHolder @@ -38,6 +38,9 @@ class Player(InventoryHolder, FightingEntity): self.max_xp = max_xp self.xp_buff = xp_buff self.inventory = self.translate_inventory(inventory or []) + b = Bow() + b.held_by=self + self.inventory.append(b) self.paths = dict() self.hazel = hazel self.equipped_main = self.dict_to_item(equipped_main) \ diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index 906d6df..cec8a4c 100644 --- a/squirrelbattle/enums.py +++ b/squirrelbattle/enums.py @@ -48,6 +48,7 @@ class KeyValues(Enum): CHAT = auto() WAIT = auto() LADDER = auto() + LAUNCH = auto() @staticmethod def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]: @@ -84,4 +85,6 @@ class KeyValues(Enum): return KeyValues.WAIT elif key == settings.KEY_LADDER: return KeyValues.LADDER + elif key == settings.KEY_LAUNCH: + return KeyValues.LAUNCH return None diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index ecdfccb..f30183e 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -35,6 +35,7 @@ class Game: """ self.state = GameMode.MAINMENU self.waiting_for_friendly_key = False + self.waiting_for_launch_key = False self.is_in_store_menu = True self.settings = Settings() self.settings.load_settings() @@ -117,6 +118,9 @@ class Game: if self.waiting_for_friendly_key: # The player requested to talk with a friendly entity self.handle_friendly_entity_chat(key) + elif self.waiting_for_launch_key: + # The player requested to launch + self.handle_launch(key) else: self.handle_key_pressed_play(key) elif self.state == GameMode.INVENTORY: @@ -155,6 +159,9 @@ class Game: self.player.equipped_main.use() if self.player.equipped_secondary: self.player.equipped_secondary.use() + elif key == KeyValues.LAUNCH: + # Wait for the direction to launch in + self.waiting_for_launch_key = True elif key == KeyValues.SPACE: self.state = GameMode.MAINMENU elif key == KeyValues.CHAT: @@ -247,6 +254,31 @@ class Game: self.store_menu.update_merchant(entity) self.display_actions(DisplayActions.UPDATE) + def handle_launch(self, key: KeyValues) -> None: + """ + If the player tries to throw something in a direction, the game looks + for entities in that direction and within the range of the player's + weapon and adds damage + """ + if not self.waiting_for_launch_key: + return + self.waiting_for_launch_key = False + + if key == KeyValues.UP: + direction = 0 + elif key == KeyValues.DOWN: + direction = 2 + elif key == KeyValues.LEFT: + direction = 3 + elif key == KeyValues.RIGHT: + direction = 1 + else: + return + + 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 814736f..2618892 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -630,7 +630,7 @@ class Entity: Trumpet from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \ - ScrollofDamage, ScrollofWeakening + ScrollofDamage, ScrollofWeakening, Ruler, Bow return { "Tiger": Tiger, "Bomb": Bomb, @@ -650,8 +650,10 @@ class Entity: "Helmet": Helmet, "RingCritical": RingCritical, "RingXP": RingXP, + "Ruler": Ruler, "ScrollofDamage": ScrollofDamage, "ScrollofWeakening": ScrollofWeakening, + "Bow": Bow, } def save_state(self) -> dict: diff --git a/squirrelbattle/settings.py b/squirrelbattle/settings.py index b5e2c14..284b41f 100644 --- a/squirrelbattle/settings.py +++ b/squirrelbattle/settings.py @@ -35,6 +35,7 @@ class Settings: 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.KEY_LAUNCH = ['l', 'Key used to use a bow'] self.TEXTURE_PACK = ['ascii', 'Texture pack'] self.LOCALE = [locale.getlocale()[0][:2], 'Language'] From 903a06c36c1c40204762d09a38e45af05aa3ce17 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 18:38:54 +0100 Subject: [PATCH 04/12] Subclassed and removed some debugging code --- squirrelbattle/entities/items.py | 38 +++++++++++++++++++++++-------- squirrelbattle/entities/player.py | 3 --- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index b223688..8310b81 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -477,13 +477,10 @@ class ScrollofWeakening(Item): _(f"The ennemies have -{max(1, self.held_by.intelligence//2)} strength for 3 turns")) self.held_by.inventory.remove(self) -class Bow(Item): - """ - A type of throwable weapon that deals damage based on the player's dexterity. - """ - def __init__(self, name: str = "bow", price: int = 22, damage = 4, - rang = 3, *args, **kwargs): - super().__init__(name=name, price=price, *args, **kwargs) +class LongRangeWeapon(Item): + def __init__(self, damage: int = 4, + rang: int = 3, *args, **kwargs): + super().__init__(*args, **kwargs) self.damage = damage self.range = rang @@ -510,11 +507,34 @@ class Bow(Item): if to_kill: self.held_by.map.logs.add_message(_("{name} is shot by an arrow.")\ .format(name=to_kill.translated_name.capitalize())+ " " \ - + to_kill.take_damage(self.held_by, self.damage + self.held_by.dexterity)) + + to_kill.take_damage(self.held_by, self.damage + getattr(self.held_by, self.stat))) def equip(self) -> None: """ - Equip the bow. + 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. + """ + +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" diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 8981341..b4be043 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -38,9 +38,6 @@ class Player(InventoryHolder, FightingEntity): self.max_xp = max_xp self.xp_buff = xp_buff self.inventory = self.translate_inventory(inventory or []) - b = Bow() - b.held_by=self - self.inventory.append(b) self.paths = dict() self.hazel = hazel self.equipped_main = self.dict_to_item(equipped_main) \ From 591630b8a7c9290db35f29ec6302fee276f5d8b6 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 19:05:02 +0100 Subject: [PATCH 05/12] Added a fire ball staff, closes #64 --- squirrelbattle/display/texturepack.py | 3 +++ squirrelbattle/entities/items.py | 39 ++++++++++++++++++++++++--- squirrelbattle/entities/player.py | 2 +- squirrelbattle/interfaces.py | 3 ++- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 0ced27f..3ba64f5 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -25,6 +25,7 @@ class TexturePack: CHESTPLATE: str EAGLE: str EMPTY: str + FIRE_BALL_STAFF: str FLOOR: str HAZELNUT: str HEART: str @@ -82,6 +83,7 @@ TexturePack.ASCII_PACK = TexturePack( EAGLE='ยต', EMPTY=' ', EXPLOSION='%', + FIRE_BALL_STAFF=':', FLOOR='.', LADDER='H', HAZELNUT='ยค', @@ -121,6 +123,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( EAGLE='๐Ÿฆ…', EMPTY=' ', EXPLOSION='๐Ÿ’ฅ', + FIRE_BALL_STAFF='๐Ÿช„', FLOOR='โ–ˆโ–ˆ', LADDER=('๐Ÿชœ', curses.COLOR_WHITE, (1000, 1000, 1000), curses.COLOR_WHITE, (1000, 1000, 1000)), diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 8310b81..9abcfc3 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -86,7 +86,7 @@ class Item(Entity): """ return [BodySnatchPotion, Bomb, Heart, Shield, Sword,\ Chestplate, Helmet, RingCritical, RingXP, \ - ScrollofDamage, ScrollofWeakening, Ruler, Bow] + ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff] def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: """ @@ -505,9 +505,10 @@ class LongRangeWeapon(Item): self.held_by.x-entity.x<=self.range: to_kill = entity if to_kill: - self.held_by.map.logs.add_message(_("{name} is shot by an arrow.")\ - .format(name=to_kill.translated_name.capitalize())+ " " \ - + to_kill.take_damage(self.held_by, self.damage + getattr(self.held_by, self.stat))) + self.held_by.map.logs.add_message(_("{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))) def equip(self) -> None: """ @@ -523,6 +524,12 @@ class LongRangeWeapon(Item): 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 @@ -538,3 +545,27 @@ class Bow(LongRangeWeapon): Here it is dexterity """ return "dexterity" + + @property + def string(self) -> str: + return " is shot by an arrow." + +class FireBallStaff(LongRangeWeapon): + """ + A type of long range weapon that deals damage based on the player's dexterity + """ + 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 dexterity + """ + return "intelligence" + + @property + def string(self) -> str: + return " is shot by a fire ball." diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index b4be043..615dfd5 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -4,7 +4,7 @@ from random import randint from typing import Dict, Optional, Tuple -from .items import Item, Bow +from .items import Item from ..interfaces import FightingEntity, InventoryHolder diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 2618892..dc4bdda 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -630,7 +630,7 @@ class Entity: Trumpet from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \ - ScrollofDamage, ScrollofWeakening, Ruler, Bow + ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff return { "Tiger": Tiger, "Bomb": Bomb, @@ -654,6 +654,7 @@ class Entity: "ScrollofDamage": ScrollofDamage, "ScrollofWeakening": ScrollofWeakening, "Bow": Bow, + "FireBallStaff": FireBallStaff, } def save_state(self) -> dict: From 746379bad63a9503e3f38abcd2dbd0701993c609 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 19:18:29 +0100 Subject: [PATCH 06/12] Now with EXPLOSIONS! --- squirrelbattle/entities/items.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 9abcfc3..51a7145 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from random import choice, randint -from typing import Optional +from typing import Optional, Any from ..interfaces import Entity, FightingEntity, Map, InventoryHolder from ..translations import gettext as _ @@ -484,7 +484,7 @@ class LongRangeWeapon(Item): self.damage = damage self.range = rang - def throw(self, direction: int) -> str: + def throw(self, direction: int) -> Any: to_kill = None for entity in self.held_by.map.entities: if entity.is_fighting_entity(): @@ -509,6 +509,7 @@ class LongRangeWeapon(Item): .format(name=to_kill.translated_name.capitalize())+ self.string + " " \ + to_kill.take_damage(self.held_by, self.damage + \ getattr(self.held_by, self.stat))) + return (to_kill.x, to_kill.y) if to_kill else None def equip(self) -> None: """ @@ -569,3 +570,15 @@ class FireBallStaff(LongRangeWeapon): @property def string(self) -> str: return " is shot by a fire ball." + + def throw(self, direction: int) -> None: + """ + Adds an explosion animation when killing something. + """ + A = super().throw(direction) + if A: + x=A[0] + y=A[1] + + explosion = Explosion(y=y, x=x) + self.held_by.map.add_entity(explosion) From 9ff615a6b0a133d31f16764f1e4911ac39804c21 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 22:25:00 +0100 Subject: [PATCH 07/12] Linting and tests... --- squirrelbattle/entities/items.py | 79 +++++++++++++--------- squirrelbattle/game.py | 1 - squirrelbattle/interfaces.py | 17 ++--- squirrelbattle/tests/game_test.py | 108 ++++++++++++++++++++++++++++-- 4 files changed, 160 insertions(+), 45 deletions(-) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 51a7145..04db192 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -84,8 +84,8 @@ class Item(Entity): """ Returns the list of all item classes. """ - return [BodySnatchPotion, Bomb, Heart, Shield, Sword,\ - Chestplate, Helmet, RingCritical, RingXP, \ + return [BodySnatchPotion, Bomb, Heart, Shield, Sword, + Chestplate, Helmet, RingCritical, RingXP, ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff] def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: @@ -256,13 +256,15 @@ class Sword(Weapon): *args, **kwargs): 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) + super().__init__(name=name, price=price, damage=damage, *args, **kwargs) + class Armor(Item): """ @@ -297,6 +299,7 @@ class Shield(Armor): super().__init__(name=name, constitution=constitution, price=price, *args, **kwargs) + class Helmet(Armor): """ Class of helmet items, they can be equipped on the head. @@ -312,6 +315,7 @@ class Helmet(Armor): 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. @@ -327,6 +331,7 @@ class Chestplate(Armor): self.held_by.remove_from_inventory(self) self.held_by.equipped_armor = self + class BodySnatchPotion(Item): """ The body-snatch potion allows to exchange all characteristics with a random @@ -436,6 +441,7 @@ class RingXP(Ring): super().__init__(name=name, price=price, experience=experience, *args, **kwargs) + class ScrollofDamage(Item): """ A scroll that, when used, deals damage to all entities in a certain radius. @@ -451,11 +457,12 @@ class ScrollofDamage(Item): """ 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(\ + 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 turn. @@ -470,13 +477,17 @@ class ScrollofWeakening(Item): """ 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")) + 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(Item): def __init__(self, damage: int = 4, rang: int = 3, *args, **kwargs): @@ -489,26 +500,28 @@ class LongRangeWeapon(Item): 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: + 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: + 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: + 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: + and self.held_by.x - entity.x > 0 and \ + self.held_by.x - entity.x <= self.range: to_kill = entity if to_kill: - self.held_by.map.logs.add_message(_("{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))) + 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.x, to_kill.y) if to_kill else None def equip(self) -> None: @@ -531,13 +544,15 @@ class LongRangeWeapon(Item): 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 + 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, \ + super().__init__(name=name, price=price, damage=damage, rang=rang, *args, **kwargs) @property @@ -551,13 +566,15 @@ class Bow(LongRangeWeapon): def string(self) -> str: return " is shot by an arrow." + class FireBallStaff(LongRangeWeapon): """ - A type of long range weapon that deals damage based on the player's dexterity + 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,\ + 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, \ + super().__init__(name=name, price=price, damage=damage, rang=rang, *args, **kwargs) @property @@ -575,10 +592,10 @@ class FireBallStaff(LongRangeWeapon): """ Adds an explosion animation when killing something. """ - A = super().throw(direction) - if A: - x=A[0] - y=A[1] + coord = super().throw(direction) + if coord: + x = coord[0] + y = coord[1] explosion = Explosion(y=y, x=x) self.held_by.map.add_entity(explosion) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index f30183e..dc866f2 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -277,7 +277,6 @@ class Game: if self.player.equipped_main: self.player.equipped_main.throw(direction) - def handle_key_pressed_inventory(self, key: KeyValues) -> None: """ diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index dc4bdda..34a4c90 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -605,7 +605,7 @@ class Entity: from squirrelbattle.entities.monsters import Tiger, Hedgehog, \ Rabbit, TeddyBear, GiantSeaEagle from squirrelbattle.entities.friendly import Merchant, Sunflower, \ - Trumpet + Trumpet return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear, Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet] @@ -697,7 +697,7 @@ class FightingEntity(Entity): self.constitution = constitution self.level = level self.critical = critical - self.effects = [] #effects are temporary buff or weakening of the stats. + self.effects = [] # effects = temporary buff or weakening of the stats. @property def dead(self) -> bool: @@ -713,12 +713,13 @@ class FightingEntity(Entity): for i in range(len(self.effects)): self.effects[i][2] -= 1 - l = self.effects[:] - for i in range(len(l)): - if l[i][2] <= 0: - setattr(self, l[i][0], getattr(self, l[i][0])-l[i][1]) - self.effects.remove(l[i]) - + copy = self.effects[:] + for i in range(len(copy)): + if copy[i][2] <= 0: + setattr(self, copy[i][0], + getattr(self, copy[i][0]) - copy[i][1]) + self.effects.remove(copy[i]) + def hit(self, opponent: "FightingEntity") -> str: """ The entity deals damage to the opponent diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 0843ae8..6fe4203 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -10,11 +10,12 @@ 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, Shield, Helmet, \ - Chestplate, RingCritical -from ..entities.monsters import GiantSeaEagle + Chestplate, RingCritical, Bow, FireBallStaff, ScrollofDamage,\ + ScrollofWeakening +from ..entities.monsters import Rabbit, GiantSeaEagle from ..entities.player import Player -from ..enums import DisplayActions -from ..game import Game, KeyValues, GameMode +from ..enums import DisplayActions, KeyValues, GameMode +from ..game import Game from ..interfaces import Map from ..menus import MainMenuValues from ..resources import ResourceManager @@ -344,7 +345,7 @@ class TestGame(unittest.TestCase): self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a') # Navigate to "texture pack" - for ignored in range(12): + for ignored in range(13): self.game.handle_key_pressed(KeyValues.DOWN) # Change texture pack @@ -756,3 +757,100 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.ENTER) self.assertEqual(self.game.state, GameMode.MAINMENU) + + def test_launch(self) -> None: + """ + Use the long range weapons to kill some entities. + """ + self.game.state = GameMode.PLAY + self.game.player.move(2, 6) + + b = Bow() + b.held_by = self.game.player + self.game.player.equipped_main = b + self.assertTrue(self.game.player.equipped_main) + + entity = Rabbit() + entity.health = 1 + self.game.map.add_entity(entity) + entity.move(3, 6) + + self.game.handle_launch(KeyValues.UP) + + self.game.waiting_for_launch_key = True + self.game.handle_key_pressed(KeyValues.CHAT) + + entity = Rabbit() + entity.health = 1 + self.game.map.add_entity(entity) + entity.move(2, 8) + self.game.waiting_for_launch_key = True + self.game.handle_key_pressed(KeyValues.RIGHT) + + entity = Rabbit() + entity.health = 1 + self.game.map.add_entity(entity) + entity.move(2, 5) + self.game.waiting_for_launch_key = True + self.game.handle_key_pressed(KeyValues.LEFT) + + key = "l" + KeyValues.translate_key(key, self.game.settings) + + self.game.handle_key_pressed(KeyValues.LAUNCH) + self.assertTrue(self.game.waiting_for_launch_key) + self.game.handle_key_pressed(KeyValues.DOWN) + + self.assertTrue(entity.dead) + + entity2 = Rabbit() + entity2.health = 1 + self.game.map.add_entity(entity2) + entity2.move(1, 6) + + b = FireBallStaff() + self.game.player.inventory.append(b) + b.held_by = self.game.player + b.equip() + + self.game.handle_key_pressed(KeyValues.LAUNCH) + self.assertTrue(self.game.waiting_for_launch_key) + self.game.handle_key_pressed(KeyValues.UP) + + self.assertTrue(entity2.dead) + + def test_scrolls(self) -> None: + """ + Use the scrolls. + """ + self.game.state = GameMode.PLAY + self.game.player.move(2, 6) + + entity = Rabbit() + self.game.map.add_entity(entity) + entity.move(3, 6) + + entity2 = GiantSeaEagle() + self.game.map.add_entity(entity2) + entity2.move(3, 8) + + scroll1 = ScrollofDamage() + scroll2 = ScrollofWeakening() + self.game.player.inventory.append(scroll1) + self.game.player.inventory.append(scroll2) + scroll1.held_by = self.game.player + scroll2.held_by = self.game.player + + scroll1.use() + self.assertTrue(entity.health != entity.maxhealth) + self.assertTrue(entity2.health != entity2.maxhealth) + + scroll2.use() + self.assertEqual(entity.strength, 0) + self.assertEqual(entity2.strength, 999) + + self.game.map.tick(self.game.player) + self.game.map.tick(self.game.player) + self.game.map.tick(self.game.player) + + self.assertEqual(entity2.effects, []) From 175706b1e486fc48e1e464cf55d8a4d58243a44e Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 22:30:30 +0100 Subject: [PATCH 08/12] Merchants had default maxhealth. --- squirrelbattle/entities/friendly.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/entities/friendly.py b/squirrelbattle/entities/friendly.py index 974fe1f..76a2071 100644 --- a/squirrelbattle/entities/friendly.py +++ b/squirrelbattle/entities/friendly.py @@ -17,8 +17,8 @@ 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) + hazel: int = 75, maxhealth = 8, *args, **kwargs): + super().__init__(name=name, maxhealth=maxhealth, *args, **kwargs) self.inventory = self.translate_inventory(inventory or []) self.hazel = hazel if not self.inventory: From bdbf214d8d02d8ecc33d54ae95e2d8188d2d22db Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 23:15:48 +0100 Subject: [PATCH 09/12] Added chests, they are immortal and contain objects the player can take for free. --- squirrelbattle/display/display_manager.py | 23 ++++++++++-- squirrelbattle/display/menudisplay.py | 44 +++++++++++++++++++++-- squirrelbattle/display/texturepack.py | 3 ++ squirrelbattle/entities/friendly.py | 33 +++++++++++++++++ squirrelbattle/entities/items.py | 9 +++-- squirrelbattle/enums.py | 1 + squirrelbattle/game.py | 39 ++++++++++++++++++++ squirrelbattle/interfaces.py | 17 ++++++--- squirrelbattle/menus.py | 22 +++++++++++- 9 files changed, 178 insertions(+), 13 deletions(-) diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index 0042615..0eceb62 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -10,7 +10,8 @@ from squirrelbattle.display.mapdisplay import MapDisplay from squirrelbattle.display.messagedisplay import MessageDisplay from squirrelbattle.display.statsdisplay import StatsDisplay from squirrelbattle.display.menudisplay import MainMenuDisplay, \ - PlayerInventoryDisplay, StoreInventoryDisplay, SettingsMenuDisplay + PlayerInventoryDisplay, StoreInventoryDisplay, SettingsMenuDisplay, \ + ChestInventoryDisplay from squirrelbattle.display.logsdisplay import LogsDisplay from squirrelbattle.display.texturepack import TexturePack from typing import Any, List @@ -29,6 +30,7 @@ class DisplayManager: self.logsdisplay = LogsDisplay(screen, pack) self.playerinventorydisplay = PlayerInventoryDisplay(screen, pack) self.storeinventorydisplay = StoreInventoryDisplay(screen, pack) + self.chestinventorydisplay = ChestInventoryDisplay(screen, pack) self.mainmenudisplay = MainMenuDisplay(self.game.main_menu, screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) @@ -40,7 +42,8 @@ class DisplayManager: self.mainmenudisplay, self.settingsmenudisplay, self.logsdisplay, self.messagedisplay, self.playerinventorydisplay, - self.storeinventorydisplay, self.creditsdisplay] + self.storeinventorydisplay, self.creditsdisplay, + self.chestinventorydisplay] self.update_game_components() def handle_display_action(self, action: DisplayActions, *params) -> None: @@ -87,7 +90,8 @@ class DisplayManager: if self.game.state == GameMode.PLAY \ or self.game.state == GameMode.INVENTORY \ - or self.game.state == GameMode.STORE: + or self.game.state == GameMode.STORE\ + or self.game.state == GameMode.CHEST: # The map pad has already the good size self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.mapdisplay.pack.tile_width @@ -124,6 +128,19 @@ class DisplayManager: pack.tile_width * (2 * self.cols // (5 * pack.tile_width))) displays.append(self.storeinventorydisplay) displays.append(self.playerinventorydisplay) + elif self.game.state == GameMode.CHEST: + self.chestinventorydisplay.refresh( + self.rows // 10, + pack.tile_width * (self.cols // (2 * pack.tile_width)), + 8 * self.rows // 10, + pack.tile_width * (2 * self.cols // (5 * pack.tile_width))) + self.playerinventorydisplay.refresh( + self.rows // 10, + pack.tile_width * (self.cols // (10 * pack.tile_width)), + 8 * self.rows // 10, + pack.tile_width * (2 * self.cols // (5 * pack.tile_width))) + displays.append(self.chestinventorydisplay) + displays.append(self.playerinventorydisplay) elif self.game.state == GameMode.MAINMENU: self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) displays.append(self.mainmenudisplay) diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index cc73010..2e870e2 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -5,7 +5,8 @@ import curses from random import randint from typing import List -from squirrelbattle.menus import Menu, MainMenu, SettingsMenu, StoreMenu +from squirrelbattle.menus import Menu, MainMenu, SettingsMenu, StoreMenu,\ + ChestMenu from .display import Box, Display from ..entities.player import Player from ..enums import KeyValues, GameMode @@ -156,13 +157,16 @@ class PlayerInventoryDisplay(MenuDisplay): player: Player = None selected: bool = True store_mode: bool = False + chest_mode: bool = False def update(self, game: Game) -> None: self.player = game.player self.update_menu(game.inventory_menu) self.store_mode = game.state == GameMode.STORE + self.chest_mode = game.state == GameMode.CHEST self.selected = game.state == GameMode.INVENTORY \ - or (self.store_mode and not game.is_in_store_menu) + or (self.store_mode and not game.is_in_store_menu)\ + or (self.chest_mode and not game.is_in_chest_menu) def update_pad(self) -> None: self.menubox.update_title(_("INVENTORY")) @@ -239,3 +243,39 @@ class StoreInventoryDisplay(MenuDisplay): self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2)) game.is_in_store_menu = True game.handle_key_pressed(KeyValues.ENTER) + +class ChestInventoryDisplay(MenuDisplay): + """ + A class to handle the display of a merchant's inventory. + """ + menu: ChestMenu + selected: bool = False + + def update(self, game: Game) -> None: + self.update_menu(game.chest_menu) + self.selected = game.is_in_chest_menu + + def update_pad(self) -> None: + self.menubox.update_title(_("CHEST")) + for i, item in enumerate(self.menu.values): + rep = self.pack[item.name.upper()] + selection = f"[{rep}]" if i == self.menu.position \ + and self.selected else f" {rep} " + self.addstr(self.pad, i + 1, 0, selection + + " " + item.translated_name.capitalize()) + + @property + def truewidth(self) -> int: + return max(1, self.height if hasattr(self, "height") else 10) + + @property + def trueheight(self) -> int: + return 2 + super().trueheight + + def handle_click(self, y: int, x: int, game: Game) -> None: + """ + We can select a menu item with the mouse. + """ + self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2)) + game.is_in_chest_menu = True + game.handle_key_pressed(KeyValues.ENTER) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 3ba64f5..328f05e 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -22,6 +22,7 @@ class TexturePack: BODY_SNATCH_POTION: str BOMB: str BOW: str + CHEST: str CHESTPLATE: str EAGLE: str EMPTY: str @@ -79,6 +80,7 @@ TexturePack.ASCII_PACK = TexturePack( BODY_SNATCH_POTION='S', BOMB='รง', BOW=')', + CHEST='โ–ก', CHESTPLATE='(', EAGLE='ยต', EMPTY=' ', @@ -119,6 +121,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( BODY_SNATCH_POTION='๐Ÿ”€', BOMB='๐Ÿ’ฃ', BOW='๐Ÿน', + CHEST='๐Ÿงฐ', CHESTPLATE='๐Ÿฆบ', EAGLE='๐Ÿฆ…', EMPTY=' ', diff --git a/squirrelbattle/entities/friendly.py b/squirrelbattle/entities/friendly.py index 76a2071..5ce9de9 100644 --- a/squirrelbattle/entities/friendly.py +++ b/squirrelbattle/entities/friendly.py @@ -38,6 +38,39 @@ class Merchant(InventoryHolder, FriendlyEntity): """ self.hazel += hz +class Chest(InventoryHolder, FriendlyEntity): + """ + A class of chest inanimate entities which contain objects. + """ + 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 []) + if not 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 + """ + return _("It's not really effective") + + @property + def dead(self) -> bool: + """ + Chest can not die + """ + return False + class Sunflower(FriendlyEntity): """ diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 04db192..f1c8002 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -88,13 +88,18 @@ class Item(Entity): Chestplate, Helmet, RingCritical, RingXP, ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff] - 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) diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index cec8a4c..f39a57f 100644 --- a/squirrelbattle/enums.py +++ b/squirrelbattle/enums.py @@ -28,6 +28,7 @@ class GameMode(Enum): SETTINGS = auto() INVENTORY = auto() STORE = auto() + CHEST = auto() CREDITS = auto() diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index dc866f2..65c18c1 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -37,6 +37,7 @@ class Game: self.waiting_for_friendly_key = False self.waiting_for_launch_key = False self.is_in_store_menu = True + self.is_in_chest_menu = True self.settings = Settings() self.settings.load_settings() self.settings.write_settings() @@ -46,6 +47,7 @@ class Game: self.settings_menu.update_values(self.settings) self.inventory_menu = menus.InventoryMenu() self.store_menu = menus.StoreMenu() + self.chest_menu = menus.ChestMenu() self.logs = Logs() self.message = None @@ -131,6 +133,8 @@ class Game: self.settings_menu.handle_key_pressed(key, raw_key, self) elif self.state == GameMode.STORE: self.handle_key_pressed_store(key) + elif self.state == GameMode.CHEST: + self.handle_key_pressed_chest(key) elif self.state == GameMode.CREDITS: self.state = GameMode.MAINMENU self.display_actions(DisplayActions.REFRESH) @@ -253,6 +257,11 @@ class Game: self.is_in_store_menu = True self.store_menu.update_merchant(entity) self.display_actions(DisplayActions.UPDATE) + elif entity.is_chest(): + self.state = GameMode.CHEST + self.is_in_chest_menu = True + self.chest_menu.update_chest(entity) + self.display_actions(DisplayActions.UPDATE) def handle_launch(self, key: KeyValues) -> None: """ @@ -332,6 +341,36 @@ class Game: self.display_actions(DisplayActions.UPDATE) # Ensure that the cursor has a good position menu.position = min(menu.position, len(menu.values) - 1) + + def handle_key_pressed_chest(self, key: KeyValues) -> None: + """ + In a chest menu, we can take or put items or close the menu. + """ + menu = self.chest_menu if self.is_in_chest_menu else self.inventory_menu + + if key == KeyValues.SPACE or key == KeyValues.INVENTORY: + self.state = GameMode.PLAY + elif key == KeyValues.UP: + menu.go_up() + elif key == KeyValues.DOWN: + menu.go_down() + elif key == KeyValues.LEFT: + self.is_in_chest_menu = False + self.display_actions(DisplayActions.UPDATE) + elif key == KeyValues.RIGHT: + self.is_in_chest_menu = True + self.display_actions(DisplayActions.UPDATE) + if menu.values and not self.player.dead: + if key == KeyValues.ENTER: + item = menu.validate() + owner = self.chest_menu.chest if self.is_in_chest_menu \ + else self.player + buyer = self.player if self.is_in_chest_menu \ + else self.chest_menu.chest + flag = item.be_sold(buyer, owner, for_free = True) + self.display_actions(DisplayActions.UPDATE) + # Ensure that the cursor has a good position + menu.position = min(menu.position, len(menu.values) - 1) def handle_key_pressed_main_menu(self, key: KeyValues) -> None: """ diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 34a4c90..0a43b9a 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -589,6 +589,13 @@ class Entity: from squirrelbattle.entities.friendly import Merchant return isinstance(self, Merchant) + def is_chest(self) -> bool: + """ + Is this entity a chest? + """ + from squirrelbattle.entities.friendly import Chest + return isinstance(self, Chest) + @property def translated_name(self) -> str: """ @@ -605,9 +612,9 @@ class Entity: from squirrelbattle.entities.monsters import Tiger, Hedgehog, \ Rabbit, TeddyBear, GiantSeaEagle from squirrelbattle.entities.friendly import Merchant, Sunflower, \ - Trumpet + Trumpet, Chest return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear, - Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet] + Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet, Chest] @staticmethod def get_weights() -> list: @@ -615,8 +622,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, 4, 1, 2] + return [3, 5, 6, 5, 5, 5, 5, 4, 3, 1, 2, 4] @staticmethod def get_all_entity_classes_in_a_dict() -> dict: @@ -627,7 +633,7 @@ class Entity: from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \ TeddyBear, GiantSeaEagle from squirrelbattle.entities.friendly import Merchant, Sunflower, \ - Trumpet + Trumpet, Chest from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \ ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff @@ -655,6 +661,7 @@ class Entity: "ScrollofWeakening": ScrollofWeakening, "Bow": Bow, "FireBallStaff": FireBallStaff, + "Chest": Chest, } def save_state(self) -> dict: diff --git a/squirrelbattle/menus.py b/squirrelbattle/menus.py index 7732642..92ead1c 100644 --- a/squirrelbattle/menus.py +++ b/squirrelbattle/menus.py @@ -6,7 +6,7 @@ from typing import Any, Optional from .display.texturepack import TexturePack from .entities.player import Player -from .entities.friendly import Merchant +from .entities.friendly import Merchant, Chest from .enums import GameMode, KeyValues, DisplayActions from .settings import Settings from .translations import gettext as _, Translator @@ -158,3 +158,23 @@ class StoreMenu(Menu): Returns the values of the menu. """ return self.merchant.inventory if self.merchant else [] + + +class ChestMenu(Menu): + """ + A special instance of a menu : the menu for the inventory of a chest. + """ + chest: Chest = None + + def update_chest(self, chest: Chest) -> None: + """ + Updates the player. + """ + self.chest = chest + + @property + def values(self) -> list: + """ + Returns the values of the menu. + """ + return self.chest.inventory if self.chest else [] From 2eb42668c838fd9922bb8e2ff5a148b7fd56f32a Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 23:32:47 +0100 Subject: [PATCH 10/12] Linting and tests for chests --- squirrelbattle/display/menudisplay.py | 3 +- squirrelbattle/entities/friendly.py | 8 ++-- squirrelbattle/entities/items.py | 2 +- squirrelbattle/game.py | 4 +- squirrelbattle/tests/game_test.py | 58 ++++++++++++++++++++++++++- 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index 2e870e2..b3001ab 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -6,7 +6,7 @@ from random import randint from typing import List from squirrelbattle.menus import Menu, MainMenu, SettingsMenu, StoreMenu,\ - ChestMenu + ChestMenu from .display import Box, Display from ..entities.player import Player from ..enums import KeyValues, GameMode @@ -244,6 +244,7 @@ class StoreInventoryDisplay(MenuDisplay): game.is_in_store_menu = True game.handle_key_pressed(KeyValues.ENTER) + class ChestInventoryDisplay(MenuDisplay): """ A class to handle the display of a merchant's inventory. diff --git a/squirrelbattle/entities/friendly.py b/squirrelbattle/entities/friendly.py index 5ce9de9..91515d2 100644 --- a/squirrelbattle/entities/friendly.py +++ b/squirrelbattle/entities/friendly.py @@ -1,4 +1,5 @@ -from ..interfaces import FriendlyEntity, InventoryHolder, Map, FightingEntity +from ..interfaces import Entity, FriendlyEntity, InventoryHolder, \ + Map, FightingEntity from ..translations import gettext as _ from .player import Player from .monsters import Monster @@ -17,7 +18,7 @@ class Merchant(InventoryHolder, FriendlyEntity): return super().keys() + ["inventory", "hazel"] def __init__(self, name: str = "merchant", inventory: list = None, - hazel: int = 75, maxhealth = 8, *args, **kwargs): + hazel: int = 75, maxhealth: int = 8, *args, **kwargs): super().__init__(name=name, maxhealth=maxhealth, *args, **kwargs) self.inventory = self.translate_inventory(inventory or []) self.hazel = hazel @@ -38,6 +39,7 @@ class Merchant(InventoryHolder, FriendlyEntity): """ self.hazel += hz + class Chest(InventoryHolder, FriendlyEntity): """ A class of chest inanimate entities which contain objects. @@ -58,7 +60,7 @@ class Chest(InventoryHolder, FriendlyEntity): """ return _("You have opened the chest") - def take_damage(self, attacker: "Entity", amount: int) -> str: + def take_damage(self, attacker: Entity, amount: int) -> str: """ A chest is not living, it can not take damage """ diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index f1c8002..c4594df 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -88,7 +88,7 @@ class Item(Entity): Chestplate, Helmet, RingCritical, RingXP, ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff] - def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder,\ + def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder, for_free: bool = False) -> bool: """ Does all necessary actions when an object is to be sold. diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 65c18c1..b576c68 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -341,7 +341,7 @@ class Game: self.display_actions(DisplayActions.UPDATE) # Ensure that the cursor has a good position menu.position = min(menu.position, len(menu.values) - 1) - + def handle_key_pressed_chest(self, key: KeyValues) -> None: """ In a chest menu, we can take or put items or close the menu. @@ -367,7 +367,7 @@ class Game: else self.player buyer = self.player if self.is_in_chest_menu \ else self.chest_menu.chest - flag = item.be_sold(buyer, owner, for_free = True) + item.be_sold(buyer, owner, for_free=True) self.display_actions(DisplayActions.UPDATE) # Ensure that the cursor has a good position menu.position = min(menu.position, len(menu.values) - 1) diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 6fe4203..a8e9c59 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -8,7 +8,7 @@ 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.friendly import Merchant, Sunflower, Chest from ..entities.items import Bomb, Heart, Sword, Explosion, Shield, Helmet, \ Chestplate, RingCritical, Bow, FireBallStaff, ScrollofDamage,\ ScrollofWeakening @@ -854,3 +854,59 @@ class TestGame(unittest.TestCase): self.game.map.tick(self.game.player) self.assertEqual(entity2.effects, []) + + def test_chests(self) -> None: + """ + Interacts with chests. + """ + self.game.state = GameMode.PLAY + + chest = Chest() + chest.move(2, 6) + self.game.map.add_entity(chest) + chest.inventory.append(FireBallStaff()) + + # Talk to merchant + self.game.handle_key_pressed(KeyValues.CHAT) + self.assertTrue(self.game.waiting_for_friendly_key) + self.game.handle_key_pressed(KeyValues.DOWN) + self.assertFalse(self.game.waiting_for_friendly_key) + self.assertEqual(self.game.state, GameMode.CHEST) + self.assertTrue(self.game.logs.messages) + + # Navigate in the menu + self.game.handle_key_pressed(KeyValues.DOWN) + self.game.handle_key_pressed(KeyValues.DOWN) + self.game.handle_key_pressed(KeyValues.LEFT) + self.assertFalse(self.game.is_in_chest_menu) + self.game.handle_key_pressed(KeyValues.RIGHT) + self.assertTrue(self.game.is_in_chest_menu) + self.game.handle_key_pressed(KeyValues.UP) + self.assertEqual(self.game.chest_menu.position, 1) + + # The second item is not a heart + chest.inventory[1] = sword = Sword() + # Take the second item by clicking on it + item = self.game.chest_menu.validate() + self.assertIn(item, chest.inventory) + self.game.display_actions(DisplayActions.MOUSE, 7, 25) + self.assertIn(item, self.game.player.inventory) + self.assertNotIn(item, chest.inventory) + + # Give an item back + self.game.inventory_menu.position = len(self.game.player.inventory) - 1 + self.game.handle_key_pressed(KeyValues.LEFT) + self.assertFalse(self.game.is_in_chest_menu) + self.assertIn(sword, self.game.player.inventory) + self.assertEqual(self.game.inventory_menu.validate(), sword) + self.game.handle_key_pressed(KeyValues.ENTER) + self.assertNotIn(sword, self.game.player.inventory) + self.assertIn(sword, chest.inventory) + + # Test immortality + self.game.player.hit(chest) + self.assertTrue(not chest.dead) + + # Exit the menu + self.game.handle_key_pressed(KeyValues.SPACE) + self.assertEqual(self.game.state, GameMode.PLAY) From cbe3e226b40f6aeca88c2abb788a4f04ac3c62cd Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Fri, 8 Jan 2021 23:51:47 +0100 Subject: [PATCH 11/12] Repaired a merge error, tests now work. --- squirrelbattle/display/menudisplay.py | 2 +- squirrelbattle/tests/game_test.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index 1099f5b..64d69b7 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -275,7 +275,7 @@ class ChestInventoryDisplay(MenuDisplay): def trueheight(self) -> int: return 2 + super().trueheight - def handle_click(self, y: int, x: int, game: Game) -> None: + def handle_click(self, y: int, x: int, attr: int, game: Game) -> None: """ We can select a menu item with the mouse. """ diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 0901067..11dd376 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -897,10 +897,11 @@ class TestGame(unittest.TestCase): # The second item is not a heart chest.inventory[1] = sword = Sword() - # Take the second item by clicking on it + # Take the second item item = self.game.chest_menu.validate() self.assertIn(item, chest.inventory) - self.game.display_actions(DisplayActions.MOUSE, 7, 25) + self.game.display_actions(DisplayActions.MOUSE, 7, 25, + curses.BUTTON1_CLICKED) self.assertIn(item, self.game.player.inventory) self.assertNotIn(item, chest.inventory) From ac4a73b2cbff72a95c640c18ff37515556fb3231 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sat, 9 Jan 2021 00:01:02 +0100 Subject: [PATCH 12/12] Final linting --- squirrelbattle/entities/items.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 2560c21..c231b53 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -474,6 +474,7 @@ class RingXP(Ring): super().__init__(name=name, price=price, experience=experience, *args, **kwargs) + class ScrollofDamage(Item): """ A scroll that, when used, deals damage to all entities in a certain radius. @@ -636,4 +637,4 @@ class FireBallStaff(LongRangeWeapon): class Monocle(Item): def __init__(self, name: str = "monocle", price: int = 10, *args, **kwargs): - super().__init__(name=name, price=price, *args, **kwargs) \ No newline at end of file + super().__init__(name=name, price=price, *args, **kwargs)