Merge branch 'master' into 'lighting'

# Conflicts:
#   squirrelbattle/display/mapdisplay.py
#   squirrelbattle/interfaces.py
This commit is contained in:
2021-01-07 16:36:54 +01:00
32 changed files with 1085 additions and 395 deletions

View File

@ -0,0 +1,97 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from ..display.display import Box, Display
from ..game import Game
from ..resources import ResourceManager
from ..translations import gettext as _
class CreditsDisplay(Display):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.box = Box(*args, **kwargs)
self.pad = self.newpad(1, 1)
self.ascii_art_displayed = False
def update(self, game: Game) -> None:
return
def display(self) -> None:
self.box.refresh(self.y, self.x, self.height, self.width)
self.box.display()
self.pad.erase()
messages = [
_("Credits"),
"",
"Squirrel Battle",
"",
_("Developers:"),
"Yohann \"ÿnérant\" D'ANELLO",
"Mathilde \"eichhornchen\" DÉPRÉS",
"Nicolas \"nicomarg\" MARGULIES",
"Charles \"charsle\" PEYRAT",
"",
_("Translators:"),
"Hugo \"ifugao\" JACOB (español)",
]
for i, msg in enumerate(messages):
self.addstr(self.pad, i + (self.height - len(messages)) // 2,
(self.width - len(msg)) // 2, msg,
bold=(i == 0), italic=(":" in msg))
if self.ascii_art_displayed:
self.display_ascii_art()
self.refresh_pad(self.pad, 0, 0, self.y + 1, self.x + 1,
self.height + self.y - 2,
self.width + self.x - 2)
def display_ascii_art(self) -> None:
with open(ResourceManager.get_asset_path("ascii-art-ecureuil.txt"))\
as f:
ascii_art = f.read().split("\n")
height, width = len(ascii_art), len(ascii_art[0])
y_offset, x_offset = (self.height - height) // 2,\
(self.width - width) // 2
for i, line in enumerate(ascii_art):
for j, c in enumerate(line):
bg_color = curses.COLOR_WHITE
fg_color = curses.COLOR_BLACK
bold = False
if c == ' ':
bg_color = curses.COLOR_BLACK
elif c == '' or c == '' or c == '':
bold = True
fg_color = curses.COLOR_WHITE
bg_color = curses.COLOR_BLACK
elif c == '|':
bold = True # c = '┃'
fg_color = (100, 700, 1000)
bg_color = curses.COLOR_BLACK
elif c == '':
fg_color = (700, 300, 0)
elif c == '':
fg_color = (700, 300, 0)
bg_color = curses.COLOR_BLACK
elif c == '':
fg_color = (350, 150, 0)
elif c == '':
fg_color = (0, 0, 0)
bg_color = curses.COLOR_BLACK
elif c == '':
c = ''
fg_color = (1000, 1000, 1000)
bg_color = curses.COLOR_BLACK
self.addstr(self.pad, y_offset + i, x_offset + j, c,
fg_color, bg_color, bold=bold)
def handle_click(self, y: int, x: int, game: Game) -> None:
if self.pad.inch(y - 1, x - 1) != ord(" "):
self.ascii_art_displayed = True

View File

@ -24,9 +24,16 @@ class Display:
self.pack = pack or TexturePack.get_pack("ascii")
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
"""
Overwrites the native curses function of the same name.
"""
return curses.newpad(height, width) if self.screen else FakePad()
def truncate(self, msg: str, height: int, width: int) -> str:
"""
Truncates a string into a string adapted to the width and height of
the screen.
"""
height = max(0, height)
width = max(0, width)
lines = msg.split("\n")
@ -36,8 +43,8 @@ class Display:
def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int:
"""
Translate a tuple (R, G, B) into a curses color index.
If we have already a color index, then nothing is processed.
Translates a tuple (R, G, B) into a curses color index.
If we already have a color index, then nothing is processed.
If this is a tuple, we construct a new color index if non-existing
and we return this index.
The values of R, G and B must be between 0 and 1000, and not
@ -66,9 +73,9 @@ class Display:
low: bool = False, right: bool = False, top: bool = False,
vertical: bool = False, chartext: bool = False) -> None:
"""
Display a message onto the pad.
Displays a message onto the pad.
If the message is too large, it is truncated vertically and horizontally
The text can be bold, italic, blinking, ... if the good parameters are
The text can be bold, italic, blinking, ... if the right parameters are
given. These parameters are translated into curses attributes.
The foreground and background colors can be given as curses constants
(curses.COLOR_*), or by giving a tuple (R, G, B) that corresponds to
@ -126,6 +133,9 @@ class Display:
def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None:
"""
Resizes a pad.
"""
self.x = x
self.y = y
self.width = width
@ -136,6 +146,9 @@ class Display:
self.pad.resize(self.height + 1, self.width + 1)
def refresh(self, *args, resize_pad: bool = True) -> None:
"""
Refreshes a pad
"""
if len(args) == 4:
self.resize(*args, resize_pad)
self.display()
@ -144,10 +157,10 @@ class Display:
window_y: int, window_x: int,
last_y: int, last_x: int) -> None:
"""
Refresh a pad on a part of the window.
Refreshes a pad on a part of the window.
The refresh starts at coordinates (top_y, top_x) from the pad,
and is drawn from (window_y, window_x) to (last_y, last_x).
If coordinates are invalid (negative indexes/length..., then nothing
If coordinates are invalid (negative indexes/length...), then nothing
is drawn and no error is raised.
"""
top_y, top_x = max(0, top_y), max(0, top_x)
@ -159,15 +172,25 @@ 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:
"""
A mouse click was performed on the coordinates (y, x) of the pad.
Maybe it can do something.
Maybe it should do something.
"""
pass
@ -181,7 +204,9 @@ class Display:
class VerticalSplit(Display):
"""
A class to split the screen in two vertically with a pretty line.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, 1)
@ -202,7 +227,9 @@ class VerticalSplit(Display):
class HorizontalSplit(Display):
"""
A class to split the screen in two horizontally with a pretty line.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(1, self.cols)
@ -223,6 +250,9 @@ class HorizontalSplit(Display):
class Box(Display):
"""
A class for pretty boxes to print menus and other content.
"""
title: str = ""
def update_title(self, title: str) -> None:

View File

@ -2,6 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.creditsdisplay import CreditsDisplay
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit, \
Display
from squirrelbattle.display.mapdisplay import MapDisplay
@ -30,17 +32,21 @@ class DisplayManager:
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
self.messagedisplay = MessageDisplay(screen=screen, pack=None)
self.messagedisplay = MessageDisplay(screen, pack)
self.hbar = HorizontalSplit(screen, pack)
self.vbar = VerticalSplit(screen, pack)
self.creditsdisplay = CreditsDisplay(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay, self.settingsmenudisplay,
self.logsdisplay, self.messagedisplay,
self.playerinventorydisplay,
self.storeinventorydisplay]
self.storeinventorydisplay, self.creditsdisplay]
self.update_game_components()
def handle_display_action(self, action: DisplayActions, *params) -> None:
"""
Handles the differents values of display action.
"""
if action == DisplayActions.REFRESH:
self.refresh()
elif action == DisplayActions.UPDATE:
@ -49,19 +55,18 @@ class DisplayManager:
self.handle_mouse_click(*params)
def update_game_components(self) -> None:
"""
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:
"""
Handles the mouse clicks.
"""
displays = self.refresh()
display = None
for d in displays:
@ -74,7 +79,11 @@ class DisplayManager:
display.handle_click(y - display.y, x - display.x, self.game)
def refresh(self) -> List[Display]:
"""
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 \
@ -97,27 +106,41 @@ 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)
elif self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.settingsmenudisplay)
elif self.game.state == GameMode.CREDITS:
self.creditsdisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.creditsdisplay)
if self.game.message:
height, width = 0, 0
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)
@ -127,7 +150,7 @@ class DisplayManager:
def resize_window(self) -> bool:
"""
If the window got resized, ensure that the screen size got updated.
When the window is resized, ensures that the screen size is updated.
"""
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
if self.screen and curses.is_term_resized(self.rows,
@ -138,8 +161,16 @@ class DisplayManager:
@property
def rows(self) -> int:
"""
Overwrites the native curses attribute of the same name,
for testing purposes.
"""
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
"""
Overwrites the native curses attribute of the same name,
for testing purposes.
"""
return curses.COLS if self.screen else 42

View File

@ -2,17 +2,23 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.display.display import Display
from squirrelbattle.game import Game
from squirrelbattle.interfaces import Logs
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,16 +3,23 @@
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:
for j in range(len(self.map.tiles)):

View File

@ -5,9 +5,10 @@ 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 ..enums import KeyValues
from ..entities.player import Player
from ..enums import KeyValues, GameMode
from ..game import Game
from ..resources import ResourceManager
from ..translations import gettext as _
@ -15,8 +16,9 @@ from ..translations import gettext as _
class MenuDisplay(Display):
"""
A class to display the menu objects
A class to display the menu objects.
"""
menu: Menu
position: int
def __init__(self, *args, **kwargs):
@ -78,8 +80,13 @@ class MenuDisplay(Display):
class SettingsMenuDisplay(MenuDisplay):
"""
A class to display specifically a settingsmenu object
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]) + (" : "
@ -91,7 +98,7 @@ class SettingsMenuDisplay(MenuDisplay):
class MainMenuDisplay(Display):
"""
A class to display specifically a mainmenu object
A class to display specifically a mainmenu object.
"""
def __init__(self, menu: MainMenu, *args):
super().__init__(*args)
@ -113,6 +120,8 @@ class MainMenuDisplay(Display):
self.addstr(self.pad, 4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i],
self.fg_color)
msg = _("Credits")
self.addstr(self.pad, self.height - 1, self.width - 1 - len(msg), msg)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)
@ -122,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
@ -133,15 +145,41 @@ class MainMenuDisplay(Display):
if y <= len(self.title):
self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000)
if y == self.height - 1 and x >= self.width - 1 - len(_("Credits")):
game.state = GameMode.CREDITS
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:
@ -156,19 +194,36 @@ 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)
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)
@ -182,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,11 +3,12 @@
import curses
from squirrelbattle.display.display import Box, Display
from squirrelbattle.game import Game
class MessageDisplay(Display):
"""
Display a message in a popup.
A class to handle the display of popup messages.
"""
def __init__(self, *args, **kwargs):
@ -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,30 +4,36 @@
import curses
from ..entities.player import Player
from ..game import Game
from ..translations import gettext as _
from .display import Display
class StatsDisplay(Display):
"""
A class to handle the display of the stats of the player.
"""
player: Player
def __init__(self, *args, **kwargs):
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 {}/{}"\
.format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health,
self.player.maxhealth)
string2 = f"{_(self.player.name).capitalize()} " \
f"-- LVL {self.player.level} -- " \
f"FLOOR {-self.player.map.floor}\n" \
f"EXP {self.player.current_xp}/{self.player.max_xp}\n" \
f"HP {self.player.health}/{self.player.maxhealth}"
self.addstr(self.pad, 0, 0, string2)
string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\
.format(self.player.strength,
self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution)
string3 = f"STR {self.player.strength}\n" \
f"INT {self.player.intelligence}\n" \
f"CHR {self.player.charisma}\n" \
f"DEX {self.player.dexterity}\n" \
f"CON {self.player.constitution}"
self.addstr(self.pad, 3, 0, string3)
inventory_str = _("Inventory:") + " "

View File

@ -6,6 +6,9 @@ from typing import Any, Union, Tuple
class TexturePack:
"""
A class to handle displaying several textures.
"""
_packs = dict()
name: str
@ -30,6 +33,7 @@ class TexturePack:
SWORD: str
TEDDY_BEAR: str
TIGER: str
TRUMPET: str
WALL: str
ASCII_PACK: "TexturePack"
@ -66,6 +70,7 @@ TexturePack.ASCII_PACK = TexturePack(
EMPTY=' ',
EXPLOSION='%',
FLOOR='.',
LADDER='H',
HAZELNUT='¤',
HEART='',
HEDGEHOG='*',
@ -76,6 +81,7 @@ TexturePack.ASCII_PACK = TexturePack(
SWORD='\u2020',
TEDDY_BEAR='8',
TIGER='n',
TRUMPET='/',
WALL='#',
)
@ -93,6 +99,7 @@ TexturePack.SQUIRREL_PACK = TexturePack(
EMPTY=' ',
EXPLOSION='💥',
FLOOR='██',
LADDER=('🪜', curses.COLOR_WHITE, curses.COLOR_WHITE),
HAZELNUT='🌰',
HEART='💜',
HEDGEHOG='🦔',
@ -100,8 +107,9 @@ TexturePack.SQUIRREL_PACK = TexturePack(
MERCHANT='🦜',
RABBIT='🐇',
SUNFLOWER='🌻',
SWORD='🗡️',
SWORD='🗡️ ',
TEDDY_BEAR='🧸',
TIGER='🐅',
TRUMPET='🎺',
WALL='🧱',
)