Merge branch 'moreitems' into 'master'

Moreitems

Closes #64 and #60

See merge request ynerant/squirrel-battle!60
This commit is contained in:
eichhornchen 2021-01-09 00:05:14 +01:00
commit 93a9e5e4c4
12 changed files with 607 additions and 38 deletions

View File

@ -10,7 +10,8 @@ from squirrelbattle.display.mapdisplay import MapDisplay
from squirrelbattle.display.messagedisplay import MessageDisplay from squirrelbattle.display.messagedisplay import MessageDisplay
from squirrelbattle.display.statsdisplay import StatsDisplay from squirrelbattle.display.statsdisplay import StatsDisplay
from squirrelbattle.display.menudisplay import MainMenuDisplay, \ from squirrelbattle.display.menudisplay import MainMenuDisplay, \
PlayerInventoryDisplay, StoreInventoryDisplay, SettingsMenuDisplay PlayerInventoryDisplay, StoreInventoryDisplay, SettingsMenuDisplay, \
ChestInventoryDisplay
from squirrelbattle.display.logsdisplay import LogsDisplay from squirrelbattle.display.logsdisplay import LogsDisplay
from squirrelbattle.display.texturepack import TexturePack from squirrelbattle.display.texturepack import TexturePack
from typing import Any, List from typing import Any, List
@ -29,6 +30,7 @@ class DisplayManager:
self.logsdisplay = LogsDisplay(screen, pack) self.logsdisplay = LogsDisplay(screen, pack)
self.playerinventorydisplay = PlayerInventoryDisplay(screen, pack) self.playerinventorydisplay = PlayerInventoryDisplay(screen, pack)
self.storeinventorydisplay = StoreInventoryDisplay(screen, pack) self.storeinventorydisplay = StoreInventoryDisplay(screen, pack)
self.chestinventorydisplay = ChestInventoryDisplay(screen, pack)
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu, self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack) screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
@ -40,7 +42,8 @@ class DisplayManager:
self.mainmenudisplay, self.settingsmenudisplay, self.mainmenudisplay, self.settingsmenudisplay,
self.logsdisplay, self.messagedisplay, self.logsdisplay, self.messagedisplay,
self.playerinventorydisplay, self.playerinventorydisplay,
self.storeinventorydisplay, self.creditsdisplay] self.storeinventorydisplay, self.creditsdisplay,
self.chestinventorydisplay]
self.update_game_components() self.update_game_components()
def handle_display_action(self, action: DisplayActions, *params) -> None: def handle_display_action(self, action: DisplayActions, *params) -> None:
@ -87,7 +90,8 @@ class DisplayManager:
if self.game.state == GameMode.PLAY \ if self.game.state == GameMode.PLAY \
or self.game.state == GameMode.INVENTORY \ 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 # The map pad has already the good size
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.mapdisplay.refresh(0, 0, self.rows * 4 // 5,
self.mapdisplay.pack.tile_width self.mapdisplay.pack.tile_width
@ -124,6 +128,19 @@ class DisplayManager:
pack.tile_width * (2 * self.cols // (5 * pack.tile_width))) pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
displays.append(self.storeinventorydisplay) displays.append(self.storeinventorydisplay)
displays.append(self.playerinventorydisplay) 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: elif self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.mainmenudisplay) displays.append(self.mainmenudisplay)

View File

@ -5,7 +5,8 @@ import curses
from random import randint from random import randint
from typing import List 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 .display import Box, Display
from ..entities.player import Player from ..entities.player import Player
from ..enums import KeyValues, GameMode from ..enums import KeyValues, GameMode
@ -156,13 +157,16 @@ class PlayerInventoryDisplay(MenuDisplay):
player: Player = None player: Player = None
selected: bool = True selected: bool = True
store_mode: bool = False store_mode: bool = False
chest_mode: bool = False
def update(self, game: Game) -> None: def update(self, game: Game) -> None:
self.player = game.player self.player = game.player
self.update_menu(game.inventory_menu) self.update_menu(game.inventory_menu)
self.store_mode = game.state == GameMode.STORE self.store_mode = game.state == GameMode.STORE
self.chest_mode = game.state == GameMode.CHEST
self.selected = game.state == GameMode.INVENTORY \ 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: def update_pad(self) -> None:
self.menubox.update_title(_("INVENTORY")) 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)) self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2))
game.is_in_store_menu = True game.is_in_store_menu = True
game.handle_key_pressed(KeyValues.ENTER) 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)

View File

@ -21,9 +21,12 @@ class TexturePack:
BODY_SNATCH_POTION: str BODY_SNATCH_POTION: str
BOMB: str BOMB: str
BOW: str
CHEST: str
CHESTPLATE: str CHESTPLATE: str
EAGLE: str EAGLE: str
EMPTY: str EMPTY: str
FIRE_BALL_STAFF: str
FLOOR: str FLOOR: str
HAZELNUT: str HAZELNUT: str
HEART: str HEART: str
@ -34,6 +37,9 @@ class TexturePack:
RABBIT: str RABBIT: str
RING_OF_CRITICAL_DAMAGE: str RING_OF_CRITICAL_DAMAGE: str
RING_OF_MORE_EXPERIENCE: str RING_OF_MORE_EXPERIENCE: str
RULER: str
SCROLL_OF_DAMAGE: str
SCROLL_OF_WEAKENING: str
SHIELD: str SHIELD: str
SUNFLOWER: str SUNFLOWER: str
SWORD: str SWORD: str
@ -73,10 +79,13 @@ TexturePack.ASCII_PACK = TexturePack(
BODY_SNATCH_POTION='S', BODY_SNATCH_POTION='S',
BOMB='ç', BOMB='ç',
BOW=')',
CHEST='',
CHESTPLATE='(', CHESTPLATE='(',
EAGLE='µ', EAGLE='µ',
EMPTY=' ', EMPTY=' ',
EXPLOSION='%', EXPLOSION='%',
FIRE_BALL_STAFF=':',
FLOOR='.', FLOOR='.',
LADDER='H', LADDER='H',
HAZELNUT='¤', HAZELNUT='¤',
@ -89,6 +98,7 @@ TexturePack.ASCII_PACK = TexturePack(
RABBIT='Y', RABBIT='Y',
RING_OF_CRITICAL_DAMAGE='o', RING_OF_CRITICAL_DAMAGE='o',
RING_OF_MORE_EXPERIENCE='o', RING_OF_MORE_EXPERIENCE='o',
RULER='\\',
SHIELD='D', SHIELD='D',
SUNFLOWER='I', SUNFLOWER='I',
SWORD='\u2020', SWORD='\u2020',
@ -96,6 +106,8 @@ TexturePack.ASCII_PACK = TexturePack(
TIGER='n', TIGER='n',
TRUMPET='/', TRUMPET='/',
WALL='#', WALL='#',
SCROLL_OF_DAMAGE=']',
SCROLL_OF_WEAKENING=']',
) )
TexturePack.SQUIRREL_PACK = TexturePack( TexturePack.SQUIRREL_PACK = TexturePack(
@ -109,10 +121,13 @@ TexturePack.SQUIRREL_PACK = TexturePack(
BODY_SNATCH_POTION='🔀', BODY_SNATCH_POTION='🔀',
BOMB='💣', BOMB='💣',
BOW='🏹',
CHEST='🧰',
CHESTPLATE='🦺', CHESTPLATE='🦺',
EAGLE='🦅', EAGLE='🦅',
EMPTY=' ', EMPTY=' ',
EXPLOSION='💥', EXPLOSION='💥',
FIRE_BALL_STAFF='🪄',
FLOOR='██', FLOOR='██',
LADDER=('🪜', curses.COLOR_WHITE, (1000, 1000, 1000), LADDER=('🪜', curses.COLOR_WHITE, (1000, 1000, 1000),
curses.COLOR_WHITE, (1000, 1000, 1000)), curses.COLOR_WHITE, (1000, 1000, 1000)),
@ -126,6 +141,7 @@ TexturePack.SQUIRREL_PACK = TexturePack(
RABBIT='🐇', RABBIT='🐇',
RING_OF_CRITICAL_DAMAGE='💍', RING_OF_CRITICAL_DAMAGE='💍',
RING_OF_MORE_EXPERIENCE='💍', RING_OF_MORE_EXPERIENCE='💍',
RULER='📏',
SHIELD='🛡️ ', SHIELD='🛡️ ',
SUNFLOWER='🌻', SUNFLOWER='🌻',
SWORD='🗡️ ', SWORD='🗡️ ',
@ -133,4 +149,6 @@ TexturePack.SQUIRREL_PACK = TexturePack(
TIGER='🐅', TIGER='🐅',
TRUMPET='🎺', TRUMPET='🎺',
WALL='🧱', WALL='🧱',
SCROLL_OF_DAMAGE='📜',
SCROLL_OF_WEAKENING='📜',
) )

View File

@ -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 ..translations import gettext as _
from .player import Player from .player import Player
from .monsters import Monster from .monsters import Monster
@ -17,8 +18,8 @@ class Merchant(InventoryHolder, FriendlyEntity):
return super().keys() + ["inventory", "hazel"] return super().keys() + ["inventory", "hazel"]
def __init__(self, name: str = "merchant", inventory: list = None, def __init__(self, name: str = "merchant", inventory: list = None,
hazel: int = 75, *args, **kwargs): hazel: int = 75, maxhealth: int = 8, *args, **kwargs):
super().__init__(name=name, *args, **kwargs) super().__init__(name=name, maxhealth=maxhealth, *args, **kwargs)
self.inventory = self.translate_inventory(inventory or []) self.inventory = self.translate_inventory(inventory or [])
self.hazel = hazel self.hazel = hazel
if not self.inventory: if not self.inventory:
@ -39,6 +40,40 @@ class Merchant(InventoryHolder, FriendlyEntity):
self.hazel += hz 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): class Sunflower(FriendlyEntity):
""" """
A friendly sunflower. A friendly sunflower.

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from random import choice, randint from random import choice, randint
from typing import Optional from typing import Optional, Any
from ..interfaces import Entity, FightingEntity, Map, InventoryHolder from ..interfaces import Entity, FightingEntity, Map, InventoryHolder
from ..translations import gettext as _ from ..translations import gettext as _
@ -47,6 +47,11 @@ class Item(Entity):
Indicates what should be done when the item is used. 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: def equip(self) -> None:
""" """
Indicates what should be done when the item is equipped. Indicates what should be done when the item is equipped.
@ -86,16 +91,22 @@ class Item(Entity):
""" """
Returns the list of all item classes. Returns the list of all item classes.
""" """
return [BodySnatchPotion, Chestplate, Bomb, Heart, Helmet, Monocle, return [BodySnatchPotion, Bomb, Bow, Chestplate, FireBallStaff,
Shield, Sword, RingCritical, RingXP] 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. Does all necessary actions when an object is to be sold.
Is overwritten by some classes that cannot exist in the player's Is overwritten by some classes that cannot exist in the player's
inventory. 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) self.hold(buyer)
seller.remove_from_inventory(self) seller.remove_from_inventory(self)
buyer.change_hazel_balance(-self.price) buyer.change_hazel_balance(-self.price)
@ -266,6 +277,15 @@ class Sword(Weapon):
super().__init__(name=name, price=price, *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): class Armor(Item):
""" """
Class of items that increase the player's constitution. Class of items that increase the player's constitution.
@ -455,6 +475,165 @@ class RingXP(Ring):
*args, **kwargs) *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): class Monocle(Item):
def __init__(self, name: str = "monocle", price: int = 10, def __init__(self, name: str = "monocle", price: int = 10,
*args, **kwargs): *args, **kwargs):

View File

@ -31,6 +31,7 @@ class Monster(FightingEntity):
By default, a monster will move randomly where it is possible By default, a monster will move randomly where it is possible
If the player is closeby, the monster runs to the player. If the player is closeby, the monster runs to the player.
""" """
super().act(m)
target = None target = None
for entity in m.entities: for entity in m.entities:
if self.distance_squared(entity) <= 25 and \ if self.distance_squared(entity) <= 25 and \

View File

@ -28,6 +28,7 @@ class GameMode(Enum):
SETTINGS = auto() SETTINGS = auto()
INVENTORY = auto() INVENTORY = auto()
STORE = auto() STORE = auto()
CHEST = auto()
CREDITS = auto() CREDITS = auto()
@ -48,6 +49,7 @@ class KeyValues(Enum):
CHAT = auto() CHAT = auto()
WAIT = auto() WAIT = auto()
LADDER = auto() LADDER = auto()
LAUNCH = auto()
@staticmethod @staticmethod
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]: def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
@ -84,4 +86,6 @@ class KeyValues(Enum):
return KeyValues.WAIT return KeyValues.WAIT
elif key == settings.KEY_LADDER: elif key == settings.KEY_LADDER:
return KeyValues.LADDER return KeyValues.LADDER
elif key == settings.KEY_LAUNCH:
return KeyValues.LAUNCH
return None return None

View File

@ -35,7 +35,9 @@ class Game:
""" """
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.waiting_for_friendly_key = False self.waiting_for_friendly_key = False
self.waiting_for_launch_key = False
self.is_in_store_menu = True self.is_in_store_menu = True
self.is_in_chest_menu = True
self.settings = Settings() self.settings = Settings()
self.settings.load_settings() self.settings.load_settings()
self.settings.write_settings() self.settings.write_settings()
@ -45,6 +47,7 @@ class Game:
self.settings_menu.update_values(self.settings) self.settings_menu.update_values(self.settings)
self.inventory_menu = menus.InventoryMenu() self.inventory_menu = menus.InventoryMenu()
self.store_menu = menus.StoreMenu() self.store_menu = menus.StoreMenu()
self.chest_menu = menus.ChestMenu()
self.logs = Logs() self.logs = Logs()
self.message = None self.message = None
@ -121,6 +124,9 @@ class Game:
if self.waiting_for_friendly_key: if self.waiting_for_friendly_key:
# The player requested to talk with a friendly entity # The player requested to talk with a friendly entity
self.handle_friendly_entity_chat(key) self.handle_friendly_entity_chat(key)
elif self.waiting_for_launch_key:
# The player requested to launch
self.handle_launch(key)
else: else:
self.handle_key_pressed_play(key) self.handle_key_pressed_play(key)
elif self.state == GameMode.INVENTORY: elif self.state == GameMode.INVENTORY:
@ -131,6 +137,8 @@ class Game:
self.settings_menu.handle_key_pressed(key, raw_key, self) self.settings_menu.handle_key_pressed(key, raw_key, self)
elif self.state == GameMode.STORE: elif self.state == GameMode.STORE:
self.handle_key_pressed_store(key) self.handle_key_pressed_store(key)
elif self.state == GameMode.CHEST:
self.handle_key_pressed_chest(key)
elif self.state == GameMode.CREDITS: elif self.state == GameMode.CREDITS:
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.display_actions(DisplayActions.REFRESH) self.display_actions(DisplayActions.REFRESH)
@ -159,6 +167,9 @@ class Game:
self.player.equipped_main.use() self.player.equipped_main.use()
if self.player.equipped_secondary: if self.player.equipped_secondary:
self.player.equipped_secondary.use() 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: elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
elif key == KeyValues.CHAT: elif key == KeyValues.CHAT:
@ -250,6 +261,35 @@ class Game:
self.is_in_store_menu = True self.is_in_store_menu = True
self.store_menu.update_merchant(entity) self.store_menu.update_merchant(entity)
self.display_actions(DisplayActions.UPDATE) 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: def handle_key_pressed_inventory(self, key: KeyValues) -> None:
""" """
@ -306,6 +346,36 @@ class Game:
# Ensure that the cursor has a good position # Ensure that the cursor has a good position
menu.position = min(menu.position, len(menu.values) - 1) 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: def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
""" """
In the main menu, we can navigate through different options. In the main menu, we can navigate through different options.

View File

@ -606,6 +606,13 @@ class Entity:
from squirrelbattle.entities.friendly import Merchant from squirrelbattle.entities.friendly import Merchant
return isinstance(self, 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 @property
def translated_name(self) -> str: def translated_name(self) -> str:
""" """
@ -622,9 +629,9 @@ class Entity:
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear, GiantSeaEagle Rabbit, TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Merchant, Sunflower, \ from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet Trumpet, Chest
return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear, return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,
Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet] Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet, Chest]
@staticmethod @staticmethod
def get_weights() -> list: def get_weights() -> list:
@ -632,8 +639,7 @@ class Entity:
Returns a weigth list associated to the above function, to Returns a weigth list associated to the above function, to
be used to spawn random entities with a certain probability. be used to spawn random entities with a certain probability.
""" """
return [3, 5, 6, 5, 5, 5, return [3, 5, 6, 5, 5, 5, 5, 4, 3, 1, 2, 4]
5, 4, 4, 1, 2]
@staticmethod @staticmethod
def get_all_entity_classes_in_a_dict() -> dict: def get_all_entity_classes_in_a_dict() -> dict:
@ -644,30 +650,37 @@ class Entity:
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear, GiantSeaEagle TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Merchant, Sunflower, \ from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet Trumpet, Chest
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \
Heart, Monocle, Sword, Shield, Chestplate, Helmet, \ Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \
RingCritical, RingXP ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff, \
Monocle
return { return {
"Bomb": Bomb,
"Chestplate": Chestplate,
"Heart": Heart,
"BodySnatchPotion": BodySnatchPotion, "BodySnatchPotion": BodySnatchPotion,
"Bomb": Bomb,
"Bow": Bow,
"Chest": Chest,
"Chestplate": Chestplate,
"Eagle": GiantSeaEagle, "Eagle": GiantSeaEagle,
"FireBallStaff": FireBallStaff,
"Heart": Heart,
"Hedgehog": Hedgehog, "Hedgehog": Hedgehog,
"Helmet": Helmet, "Helmet": Helmet,
"Player": Player,
"Merchant": Merchant, "Merchant": Merchant,
"Monocle": Monocle, "Monocle": Monocle,
"Sunflower": Sunflower, "Player": Player,
"Sword": Sword,
"Trumpet": Trumpet,
"Shield": Shield,
"TeddyBear": TeddyBear,
"Tiger": Tiger,
"Rabbit": Rabbit, "Rabbit": Rabbit,
"RingCritical": RingCritical, "RingCritical": RingCritical,
"RingXP": RingXP, "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: def save_state(self) -> dict:
@ -710,6 +723,7 @@ class FightingEntity(Entity):
self.constitution = constitution self.constitution = constitution
self.level = level self.level = level
self.critical = critical self.critical = critical
self.effects = [] # effects = temporary buff or weakening of the stats.
@property @property
def dead(self) -> bool: def dead(self) -> bool:
@ -718,13 +732,27 @@ class FightingEntity(Entity):
""" """
return self.health <= 0 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: def hit(self, opponent: "FightingEntity") -> str:
""" """
The entity deals damage to the opponent The entity deals damage to the opponent
based on their respective stats. based on their respective stats.
""" """
diceroll = randint(1, 100) diceroll = randint(1, 100)
damage = self.strength damage = max(0, self.strength)
string = " " string = " "
if diceroll <= self.critical: # It is a critical hit if diceroll <= self.critical: # It is a critical hit
damage *= 4 damage *= 4

View File

@ -6,7 +6,7 @@ from typing import Any, Optional
from .display.texturepack import TexturePack from .display.texturepack import TexturePack
from .entities.player import Player from .entities.player import Player
from .entities.friendly import Merchant from .entities.friendly import Merchant, Chest
from .enums import GameMode, KeyValues, DisplayActions from .enums import GameMode, KeyValues, DisplayActions
from .settings import Settings from .settings import Settings
from .translations import gettext as _, Translator from .translations import gettext as _, Translator
@ -158,3 +158,23 @@ class StoreMenu(Menu):
Returns the values of the menu. Returns the values of the menu.
""" """
return self.merchant.inventory if self.merchant else [] 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 []

View File

@ -35,6 +35,7 @@ class Settings:
self.KEY_CHAT = ['t', 'Key used to talk to a friendly entity'] self.KEY_CHAT = ['t', 'Key used to talk to a friendly entity']
self.KEY_WAIT = ['w', 'Key used to wait'] self.KEY_WAIT = ['w', 'Key used to wait']
self.KEY_LADDER = ['<', 'Key used to use ladders'] 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.TEXTURE_PACK = ['ascii', 'Texture pack']
self.LOCALE = [locale.getlocale()[0][:2], 'Language'] self.LOCALE = [locale.getlocale()[0][:2], 'Language']

View File

@ -8,13 +8,14 @@ import unittest
from ..bootstrap import Bootstrap from ..bootstrap import Bootstrap
from ..display.display import Display from ..display.display import Display
from ..display.display_manager import DisplayManager 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, \ from ..entities.items import Bomb, Heart, Sword, Explosion, Shield, Helmet, \
Chestplate, RingCritical, Monocle Chestplate, RingCritical, Bow, FireBallStaff, ScrollofDamage,\
from ..entities.monsters import GiantSeaEagle ScrollofWeakening, Monocle
from ..entities.monsters import Rabbit, GiantSeaEagle
from ..entities.player import Player from ..entities.player import Player
from ..enums import DisplayActions from ..enums import DisplayActions, KeyValues, GameMode
from ..game import Game, KeyValues, GameMode from ..game import Game
from ..interfaces import Map from ..interfaces import Map
from ..menus import MainMenuValues from ..menus import MainMenuValues
from ..resources import ResourceManager from ..resources import ResourceManager
@ -349,7 +350,7 @@ class TestGame(unittest.TestCase):
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a') self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
# Navigate to "texture pack" # Navigate to "texture pack"
for ignored in range(12): for ignored in range(13):
self.game.handle_key_pressed(KeyValues.DOWN) self.game.handle_key_pressed(KeyValues.DOWN)
# Change texture pack # Change texture pack
@ -767,3 +768,157 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU) 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)