diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index a1d2ed9..b4153c9 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 4e08436..64d69b7 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")) @@ -241,3 +245,40 @@ 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, attr: 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 34c76ee..d5c6da1 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -21,9 +21,12 @@ class TexturePack: BODY_SNATCH_POTION: str BOMB: str + BOW: str + CHEST: str CHESTPLATE: str EAGLE: str EMPTY: str + FIRE_BALL_STAFF: str FLOOR: str HAZELNUT: str HEART: str @@ -34,6 +37,9 @@ 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 SUNFLOWER: str SWORD: str @@ -73,10 +79,13 @@ TexturePack.ASCII_PACK = TexturePack( BODY_SNATCH_POTION='S', BOMB='รง', + BOW=')', + CHEST='โ–ก', CHESTPLATE='(', EAGLE='ยต', EMPTY=' ', EXPLOSION='%', + FIRE_BALL_STAFF=':', FLOOR='.', LADDER='H', HAZELNUT='ยค', @@ -89,6 +98,7 @@ TexturePack.ASCII_PACK = TexturePack( RABBIT='Y', RING_OF_CRITICAL_DAMAGE='o', RING_OF_MORE_EXPERIENCE='o', + RULER='\\', SHIELD='D', SUNFLOWER='I', SWORD='\u2020', @@ -96,6 +106,8 @@ TexturePack.ASCII_PACK = TexturePack( TIGER='n', TRUMPET='/', WALL='#', + SCROLL_OF_DAMAGE=']', + SCROLL_OF_WEAKENING=']', ) TexturePack.SQUIRREL_PACK = TexturePack( @@ -109,10 +121,13 @@ TexturePack.SQUIRREL_PACK = TexturePack( BODY_SNATCH_POTION='๐Ÿ”€', BOMB='๐Ÿ’ฃ', + BOW='๐Ÿน', + CHEST='๐Ÿงฐ', CHESTPLATE='๐Ÿฆบ', EAGLE='๐Ÿฆ…', EMPTY=' ', EXPLOSION='๐Ÿ’ฅ', + FIRE_BALL_STAFF='๐Ÿช„', FLOOR='โ–ˆโ–ˆ', LADDER=('๐Ÿชœ', curses.COLOR_WHITE, (1000, 1000, 1000), curses.COLOR_WHITE, (1000, 1000, 1000)), @@ -126,6 +141,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( RABBIT='๐Ÿ‡', RING_OF_CRITICAL_DAMAGE='๐Ÿ’', RING_OF_MORE_EXPERIENCE='๐Ÿ’', + RULER='๐Ÿ“', SHIELD='๐Ÿ›ก๏ธ ', SUNFLOWER='๐ŸŒป', SWORD='๐Ÿ—ก๏ธ ', @@ -133,4 +149,6 @@ TexturePack.SQUIRREL_PACK = TexturePack( TIGER='๐Ÿ…', TRUMPET='๐ŸŽบ', WALL='๐Ÿงฑ', + SCROLL_OF_DAMAGE='๐Ÿ“œ', + SCROLL_OF_WEAKENING='๐Ÿ“œ', ) diff --git a/squirrelbattle/entities/friendly.py b/squirrelbattle/entities/friendly.py index 974fe1f..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,8 +18,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: int = 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: @@ -39,6 +40,40 @@ 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): """ A friendly sunflower. diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index 25244cd..c231b53 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 _ @@ -47,6 +47,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. @@ -86,16 +91,22 @@ class Item(Entity): """ Returns the list of all item classes. """ - return [BodySnatchPotion, Chestplate, Bomb, Heart, Helmet, Monocle, - Shield, Sword, RingCritical, RingXP] + return [BodySnatchPotion, Bomb, Bow, Chestplate, FireBallStaff, + Heart, Helmet, Monocle, ScrollofDamage, ScrollofWeakening, + Shield, Sword, RingCritical, RingXP, Ruler] - def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: + def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder, + for_free: bool = False) -> bool: """ Does all necessary actions when an object is to be sold. Is overwritten by some classes that cannot exist in the player's inventory. """ - if buyer.hazel >= self.price: + if for_free: + self.hold(buyer) + seller.remove_from_inventory(self) + return True + elif buyer.hazel >= self.price: self.hold(buyer) seller.remove_from_inventory(self) buyer.change_hazel_balance(-self.price) @@ -266,6 +277,15 @@ class Sword(Weapon): super().__init__(name=name, price=price, *args, **kwargs) +class Ruler(Weapon): + """ + A basic weapon + """ + def __init__(self, name: str = "ruler", price: int = 2, + damage: int = 1, *args, **kwargs): + super().__init__(name=name, price=price, damage=damage, *args, **kwargs) + + class Armor(Item): """ Class of items that increase the player's constitution. @@ -455,6 +475,165 @@ class RingXP(Ring): *args, **kwargs) +class ScrollofDamage(Item): + """ + A scroll that, when used, deals damage to all entities in a certain radius. + """ + def __init__(self, name: str = "scroll_of_damage", price: int = 18, + *args, **kwargs): + super().__init__(name=name, price=price, *args, **kwargs) + + def use(self) -> None: + """ + Find all entities within a radius of 5, and deal damage based on the + player's intelligence. + """ + for entity in self.held_by.map.entities: + if entity.is_fighting_entity() and not entity == self.held_by: + if entity.distance(self.held_by) <= 5: + self.held_by.map.logs.add_message(entity.take_damage( + self.held_by, self.held_by.intelligence)) + self.held_by.inventory.remove(self) + + +class ScrollofWeakening(Item): + """ + A scroll that, when used, reduces the damage of the ennemies for 3 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) + + +class LongRangeWeapon(Item): + def __init__(self, damage: int = 4, + rang: int = 3, *args, **kwargs): + super().__init__(*args, **kwargs) + self.damage = damage + self.range = rang + + def throw(self, direction: int) -> Any: + to_kill = None + for entity in self.held_by.map.entities: + if entity.is_fighting_entity(): + if direction == 0 and self.held_by.x == entity.x \ + and self.held_by.y - entity.y > 0 and \ + self.held_by.y - entity.y <= self.range: + to_kill = entity + elif direction == 2 and self.held_by.x == entity.x \ + and entity.y - self.held_by.y > 0 and \ + entity.y - self.held_by.y <= self.range: + to_kill = entity + elif direction == 1 and self.held_by.y == entity.y \ + and entity.x - self.held_by.x > 0 and \ + entity.x - self.held_by.x <= self.range: + to_kill = entity + elif direction == 3 and self.held_by.y == entity.y \ + and self.held_by.x - entity.x > 0 and \ + self.held_by.x - entity.x <= self.range: + to_kill = entity + if to_kill: + line = _("{name}").format(name=to_kill.translated_name.capitalize() + ) + self.string + " "\ + + to_kill.take_damage( + self.held_by, self.damage + + getattr(self.held_by, self.stat)) + self.held_by.map.logs.add_message(line) + return (to_kill.x, to_kill.y) if to_kill else None + + def equip(self) -> None: + """ + Equip the weapon. + """ + self.held_by.remove_from_inventory(self) + self.held_by.equipped_main = self + + @property + def stat(self) -> str: + """ + The stat that is used when using the object: dexterity for a bow + or intelligence for a magic staff. + """ + + @property + def string(self) -> str: + """ + The string that is printed when we hit an ennemy. + """ + + +class Bow(LongRangeWeapon): + """ + A type of long range weapon that deals damage + based on the player's dexterity + """ + def __init__(self, name: str = "bow", price: int = 22, damage: int = 4, + rang: int = 3, *args, **kwargs): + super().__init__(name=name, price=price, damage=damage, + rang=rang, *args, **kwargs) + + @property + def stat(self) -> str: + """ + Here it is dexterity + """ + return "dexterity" + + @property + def string(self) -> str: + return " is shot by an arrow." + + +class FireBallStaff(LongRangeWeapon): + """ + A type of powerful long range weapon that deals damage + based on the player's intelligence + """ + def __init__(self, name: str = "fire_ball_staff", price: int = 36, + damage: int = 6, rang: int = 4, *args, **kwargs): + super().__init__(name=name, price=price, damage=damage, + rang=rang, *args, **kwargs) + + @property + def stat(self) -> str: + """ + Here it is dexterity + """ + return "intelligence" + + @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. + """ + 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) + + class Monocle(Item): def __init__(self, name: str = "monocle", price: int = 10, *args, **kwargs): diff --git a/squirrelbattle/entities/monsters.py b/squirrelbattle/entities/monsters.py index 374b1d6..b62421a 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/enums.py b/squirrelbattle/enums.py index 906d6df..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() @@ -48,6 +49,7 @@ class KeyValues(Enum): CHAT = auto() WAIT = auto() LADDER = auto() + LAUNCH = auto() @staticmethod def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]: @@ -84,4 +86,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 837e753..9734144 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -35,7 +35,9 @@ 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.is_in_chest_menu = True self.settings = Settings() self.settings.load_settings() self.settings.write_settings() @@ -45,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 @@ -121,6 +124,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: @@ -131,6 +137,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) @@ -159,6 +167,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: @@ -250,6 +261,35 @@ 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: + """ + 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: """ @@ -306,6 +346,36 @@ class Game: # 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 + 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: """ In the main menu, we can navigate through different options. diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 17e5cee..828fe59 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -606,6 +606,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: """ @@ -622,9 +629,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: @@ -632,8 +639,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: @@ -644,30 +650,37 @@ 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, Monocle, Sword, Shield, Chestplate, Helmet, \ - RingCritical, RingXP + Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \ + ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff, \ + Monocle return { - "Bomb": Bomb, - "Chestplate": Chestplate, - "Heart": Heart, "BodySnatchPotion": BodySnatchPotion, + "Bomb": Bomb, + "Bow": Bow, + "Chest": Chest, + "Chestplate": Chestplate, "Eagle": GiantSeaEagle, + "FireBallStaff": FireBallStaff, + "Heart": Heart, "Hedgehog": Hedgehog, "Helmet": Helmet, - "Player": Player, "Merchant": Merchant, "Monocle": Monocle, - "Sunflower": Sunflower, - "Sword": Sword, - "Trumpet": Trumpet, - "Shield": Shield, - "TeddyBear": TeddyBear, - "Tiger": Tiger, + "Player": Player, "Rabbit": Rabbit, "RingCritical": RingCritical, "RingXP": RingXP, + "Ruler": Ruler, + "ScrollofDamage": ScrollofDamage, + "ScrollofWeakening": ScrollofWeakening, + "Shield": Shield, + "Sunflower": Sunflower, + "Sword": Sword, + "Trumpet": Trumpet, + "TeddyBear": TeddyBear, + "Tiger": Tiger, } def save_state(self) -> dict: @@ -710,6 +723,7 @@ class FightingEntity(Entity): self.constitution = constitution self.level = level self.critical = critical + self.effects = [] # effects = temporary buff or weakening of the stats. @property def dead(self) -> bool: @@ -718,13 +732,27 @@ 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 + + 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 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 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 [] diff --git a/squirrelbattle/settings.py b/squirrelbattle/settings.py index 549fc5f..6f7cf57 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'] diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index a5cb8ae..11dd376 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -8,13 +8,14 @@ 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, Monocle -from ..entities.monsters import GiantSeaEagle + Chestplate, RingCritical, Bow, FireBallStaff, ScrollofDamage,\ + ScrollofWeakening, Monocle +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 @@ -349,7 +350,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 @@ -767,3 +768,157 @@ 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, []) + + 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 + item = self.game.chest_menu.validate() + self.assertIn(item, chest.inventory) + self.game.display_actions(DisplayActions.MOUSE, 7, 25, + curses.BUTTON1_CLICKED) + 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)