Merge branch 'master' into 'familiars'

# Conflicts:
#   squirrelbattle/display/display_manager.py
#   squirrelbattle/display/logsdisplay.py
#   squirrelbattle/display/mapdisplay.py
#   squirrelbattle/display/menudisplay.py
#   squirrelbattle/menus.py
This commit is contained in:
eichhornchen 2021-01-05 10:27:39 +01:00
commit 7f63ab2357
15 changed files with 167 additions and 57 deletions

View File

@ -172,9 +172,19 @@ class Display:
if last_y >= window_y and last_x >= window_x:
# Refresh the pad only if coordinates are valid
pad.refresh(top_y, top_x, window_y, window_x, last_y, last_x)
pad.noutrefresh(top_y, top_x, window_y, window_x, last_y, last_x)
def display(self) -> None:
"""
Draw the content of the display and refresh pads.
"""
raise NotImplementedError
def update(self, game: Game) -> None:
"""
The game state was updated.
Indicate what to do with the new state.
"""
raise NotImplementedError
def handle_click(self, y: int, x: int, game: Game) -> None:

View File

@ -56,19 +56,12 @@ class DisplayManager:
def update_game_components(self) -> None:
"""
Updates the game components, for example when loading a game.
The game state was updated.
Trigger all displays of these modifications.
"""
for d in self.displays:
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay.update_map(self.game.map)
self.statsdisplay.update_player(self.game.player)
self.game.inventory_menu.update_player(self.game.player)
self.game.store_menu.update_merchant(self.game.player)
self.playerinventorydisplay.update_menu(self.game.inventory_menu)
self.storeinventorydisplay.update_menu(self.game.store_menu)
self.settingsmenudisplay.update_menu(self.game.settings_menu)
self.logsdisplay.update_logs(self.game.logs)
self.messagedisplay.update_message(self.game.message)
d.update(self.game)
def handle_mouse_click(self, y: int, x: int) -> None:
"""
@ -90,6 +83,7 @@ class DisplayManager:
Refreshes all components on the screen.
"""
displays = []
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
if self.game.state == GameMode.PLAY \
or self.game.state == GameMode.INVENTORY \
@ -112,14 +106,24 @@ class DisplayManager:
if self.game.state == GameMode.INVENTORY:
self.playerinventorydisplay.refresh(
self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5)
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)))
displays.append(self.playerinventorydisplay)
elif self.game.state == GameMode.STORE:
self.storeinventorydisplay.refresh(
self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5)
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.storeinventorydisplay)
displays.append(self.playerinventorydisplay)
elif self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.mainmenudisplay)
@ -135,7 +139,8 @@ class DisplayManager:
for line in self.game.message.split("\n"):
height += 1
width = max(width, len(line))
y, x = (self.rows - height) // 2, (self.cols - width) // 2
y = pack.tile_width * (self.rows - height) // (2 * pack.tile_width)
x = pack.tile_width * ((self.cols - width) // (2 * pack.tile_width))
self.messagedisplay.refresh(y, x, height, width)
displays.append(self.messagedisplay)

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.display.display import Display
from squirrelbattle.game import Game
from squirrelbattle.interfaces import Logs
@ -9,12 +10,14 @@ class LogsDisplay(Display):
"""
A class to handle the display of the logs.
"""
logs: Logs
def __init__(self, *args) -> None:
super().__init__(*args)
self.pad = self.newpad(self.rows, self.cols)
def update_logs(self, logs: Logs) -> None:
self.logs = logs
def update(self, game: Game) -> None:
self.logs = game.logs
def display(self) -> None:
messages = self.logs.messages[-self.height:]

View File

@ -3,20 +3,25 @@
from squirrelbattle.interfaces import Map
from .display import Display
from ..game import Game
class MapDisplay(Display):
"""
A class to handle the display of the map.
"""
map: Map
def __init__(self, *args):
super().__init__(*args)
def update_map(self, m: Map) -> None:
self.map = m
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
def update(self, game: Game) -> None:
self.map = game.map
self.pad = self.newpad(self.map.height,
self.pack.tile_width * self.map.width + 1)
def update_pad(self) -> None:
self.pad.resize(500, 500)
self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.pack.tile_fg_color, self.pack.tile_bg_color)
for e in self.map.entities:

View File

@ -5,8 +5,9 @@ import curses
from random import randint
from typing import List
from squirrelbattle.menus import Menu, MainMenu
from squirrelbattle.menus import Menu, MainMenu, SettingsMenu, StoreMenu
from .display import Box, Display
from ..entities.player import Player
from ..enums import KeyValues, GameMode
from ..game import Game
from ..resources import ResourceManager
@ -17,6 +18,7 @@ class MenuDisplay(Display):
"""
A class to display the menu objects.
"""
menu: Menu
position: int
def __init__(self, *args, **kwargs):
@ -80,6 +82,11 @@ class SettingsMenuDisplay(MenuDisplay):
"""
A class to display specifically a settingsmenu object.
"""
menu: SettingsMenu
def update(self, game: Game) -> None:
self.update_menu(game.settings_menu)
@property
def values(self) -> List[str]:
return [_(a[1][1]) + (" : "
@ -124,6 +131,9 @@ class MainMenuDisplay(Display):
menuy, menux, min(self.menudisplay.preferred_height,
self.height - menuy), menuwidth)
def update(self, game: Game) -> None:
self.menudisplay.update_menu(game.main_menu)
def handle_click(self, y: int, x: int, game: Game) -> None:
menuwidth = min(self.menudisplay.preferred_width, self.width)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
@ -143,13 +153,33 @@ class PlayerInventoryDisplay(MenuDisplay):
"""
A class to handle the display of the player's inventory.
"""
player: Player = None
selected: bool = True
store_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.selected = game.state == GameMode.INVENTORY \
or (self.store_mode and not game.is_in_store_menu)
def update_pad(self) -> None:
self.menubox.update_title(_("INVENTORY"))
for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()]
selection = f"[{rep}]" if i == self.menu.position else f" {rep} "
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())
+ " " + item.translated_name.capitalize()
+ (": " + str(item.price) + " Hazels"
if self.store_mode else ""))
if self.store_mode:
price = f"{self.pack.HAZELNUT} {self.player.hazel} Hazels"
width = len(price) + (self.pack.tile_width - 1)
self.addstr(self.pad, self.height - 3, self.width - width - 2,
price, italic=True)
@property
def truewidth(self) -> int:
@ -164,6 +194,7 @@ class PlayerInventoryDisplay(MenuDisplay):
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_store_menu = False
game.handle_key_pressed(KeyValues.ENTER)
@ -171,15 +202,28 @@ class StoreInventoryDisplay(MenuDisplay):
"""
A class to handle the display of a merchant's inventory.
"""
menu: StoreMenu
selected: bool = False
def update(self, game: Game) -> None:
self.update_menu(game.store_menu)
self.selected = game.is_in_store_menu
def update_pad(self) -> None:
self.menubox.update_title(_("STALL"))
for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()]
selection = f"[{rep}]" if i == self.menu.position else f" {rep} "
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()
+ ": " + str(item.price) + " Hazels")
price = f"{self.pack.HAZELNUT} {self.menu.merchant.hazel} Hazels"
width = len(price) + (self.pack.tile_width - 1)
self.addstr(self.pad, self.height - 3, self.width - width - 2, price,
italic=True)
@property
def truewidth(self) -> int:
return max(1, self.height if hasattr(self, "height") else 10)
@ -193,4 +237,5 @@ class StoreInventoryDisplay(MenuDisplay):
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_store_menu = True
game.handle_key_pressed(KeyValues.ENTER)

View File

@ -3,6 +3,7 @@
import curses
from squirrelbattle.display.display import Box, Display
from squirrelbattle.game import Game
class MessageDisplay(Display):
@ -17,8 +18,8 @@ class MessageDisplay(Display):
self.message = ""
self.pad = self.newpad(1, 1)
def update_message(self, msg: str) -> None:
self.message = msg
def update(self, game: Game) -> None:
self.message = game.message
def display(self) -> None:
self.box.refresh(self.y - 1, self.x - 2,

View File

@ -4,6 +4,7 @@
import curses
from ..entities.player import Player
from ..game import Game
from ..translations import gettext as _
from .display import Display
@ -18,8 +19,8 @@ class StatsDisplay(Display):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
def update_player(self, p: Player) -> None:
self.player = p
def update(self, game: Game) -> None:
self.player = game.player
def update_pad(self) -> None:
string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\

View File

@ -102,7 +102,7 @@ TexturePack.SQUIRREL_PACK = TexturePack(
MERCHANT='🦜',
RABBIT='🐇',
SUNFLOWER='🌻',
SWORD='🗡️',
SWORD='🗡️ ',
TEDDY_BEAR='🧸',
TIGER='🐅',
TRUMPET='🎺',

View File

@ -34,6 +34,7 @@ class Game:
"""
self.state = GameMode.MAINMENU
self.waiting_for_friendly_key = False
self.is_in_store_menu = True
self.settings = Settings()
self.settings.load_settings()
self.settings.write_settings()
@ -51,7 +52,7 @@ class Game:
Creates a new game on the screen.
"""
# TODO generate a new map procedurally
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.map = Map.load(ResourceManager.get_asset_path("example_map_2.txt"))
self.map.logs = self.logs
self.logs.clear()
self.player = Player()
@ -60,16 +61,18 @@ class Game:
self.map.spawn_random_entities(randint(3, 10))
self.inventory_menu.update_player(self.player)
def run(self, screen: Any) -> None:
def run(self, screen: Any) -> None: # pragma no cover
"""
Main infinite loop.
We wait for the player's action, then we do what should be done
when a key gets pressed.
"""
while True: # pragma no cover
screen.refresh()
while True:
screen.erase()
screen.refresh()
screen.noutrefresh()
self.display_actions(DisplayActions.REFRESH)
curses.doupdate()
key = screen.getkey()
if key == "KEY_MOUSE":
_ignored1, x, y, _ignored2, _ignored3 = curses.getmouse()
@ -165,7 +168,9 @@ class Game:
self.logs.add_message(msg)
if entity.is_merchant():
self.state = GameMode.STORE
self.is_in_store_menu = True
self.store_menu.update_merchant(entity)
self.display_actions(DisplayActions.UPDATE)
def handle_key_pressed_inventory(self, key: KeyValues) -> None:
"""
@ -194,22 +199,33 @@ class Game:
"""
In a store menu, we can buy items or close the menu.
"""
if key == KeyValues.SPACE:
menu = self.store_menu if self.is_in_store_menu else self.inventory_menu
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
self.state = GameMode.PLAY
elif key == KeyValues.UP:
self.store_menu.go_up()
menu.go_up()
elif key == KeyValues.DOWN:
self.store_menu.go_down()
if self.store_menu.values and not self.player.dead:
menu.go_down()
elif key == KeyValues.LEFT:
self.is_in_store_menu = False
self.display_actions(DisplayActions.UPDATE)
elif key == KeyValues.RIGHT:
self.is_in_store_menu = True
self.display_actions(DisplayActions.UPDATE)
if menu.values and not self.player.dead:
if key == KeyValues.ENTER:
item = self.store_menu.validate()
flag = item.be_sold(self.player, self.store_menu.merchant)
item = menu.validate()
owner = self.store_menu.merchant if self.is_in_store_menu \
else self.player
buyer = self.player if self.is_in_store_menu \
else self.store_menu.merchant
flag = item.be_sold(buyer, owner)
if not flag:
self.message = _("You do not have enough money")
self.display_actions(DisplayActions.UPDATE)
self.message = _("The buyer does not have enough money")
self.display_actions(DisplayActions.UPDATE)
# Ensure that the cursor has a good position
self.store_menu.position = min(self.store_menu.position,
len(self.store_menu.values) - 1)
menu.position = min(menu.position, len(menu.values) - 1)
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
"""

View File

@ -59,8 +59,8 @@ msgid "{player} exchanged its body with {entity}."
msgstr "{player} täuscht seinem Körper mit {entity} aus."
#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573
msgid "You do not have enough money"
msgstr "Sie haben nicht genug Geld"
msgid "The buyer does not have enough money"
msgstr "Der Kaufer hat nicht genug Geld"
#: squirrelbattle/game.py:249
msgid ""

View File

@ -58,8 +58,8 @@ msgid "{player} exchanged its body with {entity}."
msgstr "{player} intercambió su cuerpo con {entity}."
#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573
msgid "You do not have enough money"
msgstr "No tienes suficiente dinero"
msgid "The buyer does not have enough money"
msgstr "El comprador no tiene suficiente dinero"
#: squirrelbattle/game.py:249
msgid ""

View File

@ -59,8 +59,8 @@ msgid "{player} exchanged its body with {entity}."
msgstr "{player} a échangé son corps avec {entity}."
#: squirrelbattle/game.py:205 squirrelbattle/tests/game_test.py:573
msgid "You do not have enough money"
msgstr "Vous n'avez pas assez d'argent"
msgid "The buyer does not have enough money"
msgstr "L'acheteur n'a pas assez d'argent"
#: squirrelbattle/game.py:249
msgid ""

View File

@ -144,7 +144,7 @@ class StoreMenu(Menu):
"""
A special instance of a menu : the menu for the inventory of a merchant.
"""
merchant: Merchant
merchant: Merchant = None
def update_merchant(self, merchant: Merchant) -> None:
"""
@ -157,4 +157,4 @@ class StoreMenu(Menu):
"""
Returns the values of the menu.
"""
return self.merchant.inventory
return self.merchant.inventory if self.merchant else []

View File

@ -12,6 +12,7 @@ from ..entities.items import Bomb, Heart, Sword, Explosion
from ..entities.player import Player
from ..enums import DisplayActions
from ..game import Game, KeyValues, GameMode
from ..interfaces import Map
from ..menus import MainMenuValues
from ..resources import ResourceManager
from ..settings import Settings
@ -25,6 +26,10 @@ class TestGame(unittest.TestCase):
"""
self.game = Game()
self.game.new_game()
self.game.map = Map.load(
ResourceManager.get_asset_path("example_map.txt"))
self.game.map.add_entity(self.game.player)
self.game.player.move(self.game.map.start_y, self.game.map.start_x)
self.game.logs.add_message("Hello World !")
display = DisplayManager(None, self.game)
self.game.display_actions = display.handle_display_action
@ -404,6 +409,7 @@ class TestGame(unittest.TestCase):
Checks that some functions are not implemented, only for coverage.
"""
self.assertRaises(NotImplementedError, Display.display, None)
self.assertRaises(NotImplementedError, Display.update, None, self.game)
def test_messages(self) -> None:
"""
@ -551,18 +557,21 @@ class TestGame(unittest.TestCase):
# 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_store_menu)
self.game.handle_key_pressed(KeyValues.RIGHT)
self.assertTrue(self.game.is_in_store_menu)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.store_menu.position, 1)
self.game.player.hazel = 0x7ffff42ff
# The second item is not a heart
merchant.inventory[1] = Sword()
merchant.inventory[1] = sword = Sword()
# Buy the second item by clicking on it
item = self.game.store_menu.validate()
self.assertIn(item, merchant.inventory)
self.game.display_actions(DisplayActions.MOUSE, 7, 25)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertIn(item, self.game.player.inventory)
self.assertNotIn(item, merchant.inventory)
@ -584,9 +593,24 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertNotIn(item, self.game.player.inventory)
self.assertIn(item, merchant.inventory)
self.assertEqual(self.game.message, _("You do not have enough money"))
self.assertEqual(self.game.message,
_("The buyer does not have enough money"))
self.game.handle_key_pressed(KeyValues.ENTER)
# Sell an item
self.game.inventory_menu.position = len(self.game.player.inventory) - 1
self.game.handle_key_pressed(KeyValues.LEFT)
self.assertFalse(self.game.is_in_store_menu)
self.assertIn(sword, self.game.player.inventory)
self.assertEqual(self.game.inventory_menu.validate(), sword)
old_player_money, old_merchant_money = self.game.player.hazel,\
merchant.hazel
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertNotIn(sword, self.game.player.inventory)
self.assertIn(sword, merchant.inventory)
self.assertEqual(self.game.player.hazel, old_player_money + sword.price)
self.assertEqual(merchant.hazel, old_merchant_money - sword.price)
# Exit the menu
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.PLAY)

View File

@ -12,8 +12,8 @@ class FakePad:
def addstr(self, y: int, x: int, message: str, color: int = 0) -> None:
pass
def refresh(self, pminrow: int, pmincol: int, sminrow: int,
smincol: int, smaxrow: int, smaxcol: int) -> None:
def noutrefresh(self, pminrow: int, pmincol: int, sminrow: int,
smincol: int, smaxrow: int, smaxcol: int) -> None:
pass
def erase(self) -> None: