Merge branch 'change_image_color' into 'master'

Better color support

Closes #43

See merge request ynerant/squirrel-battle!46
This commit is contained in:
ynerant 2020-12-12 13:53:04 +01:00
commit 1986630da1
6 changed files with 110 additions and 28 deletions

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from typing import Any, Optional, Union from typing import Any, Optional, Tuple, Union
from squirrelbattle.display.texturepack import TexturePack from squirrelbattle.display.texturepack import TexturePack
from squirrelbattle.game import Game from squirrelbattle.game import Game
@ -16,6 +16,9 @@ class Display:
height: int height: int
pad: Any pad: Any
_color_pairs = {(curses.COLOR_WHITE, curses.COLOR_BLACK): 0}
_colors_rgb = {}
def __init__(self, screen: Any, pack: Optional[TexturePack] = None): def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
self.screen = screen self.screen = screen
self.pack = pack or TexturePack.get_pack("ascii") self.pack = pack or TexturePack.get_pack("ascii")
@ -31,15 +34,84 @@ class Display:
lines = [line[:width] for line in lines] lines = [line[:width] for line in lines]
return "\n".join(lines) return "\n".join(lines)
def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None: 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.
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
between 0 and 255.
"""
if isinstance(color, tuple):
# The color is a tuple (R, G, B), that is potentially unknown.
# We translate it into a curses color number.
if color not in self._colors_rgb:
# The color does not exist, we create it.
color_nb = len(self._colors_rgb) + 8
self.init_color(color_nb, color[0], color[1], color[2])
self._colors_rgb[color] = color_nb
color = self._colors_rgb[color]
return color
def addstr(self, pad: Any, y: int, x: int, msg: str,
fg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_WHITE,
bg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_BLACK,
*, altcharset: bool = False, blink: bool = False,
bold: bool = False, dim: bool = False, invis: bool = False,
italic: bool = False, normal: bool = False,
protect: bool = False, reverse: bool = False,
standout: bool = False, underline: bool = False,
horizontal: bool = False, left: bool = False,
low: bool = False, right: bool = False, top: bool = False,
vertical: bool = False, chartext: bool = False) -> None:
""" """
Display a message onto the pad. Display a message onto the pad.
If the message is too large, it is truncated vertically and horizontally If the message is too large, it is truncated vertically and horizontally
The text can be bold, italic, blinking, ... if the good 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
the color. R, G, B must be between 0 and 1000, and not 0 and 255.
""" """
height, width = pad.getmaxyx() height, width = pad.getmaxyx()
# Truncate message if it is too large
msg = self.truncate(msg, height - y, width - x - 1) msg = self.truncate(msg, height - y, width - x - 1)
if msg.replace("\n", "") and x >= 0 and y >= 0: if msg.replace("\n", "") and x >= 0 and y >= 0:
return pad.addstr(y, x, msg, *options) fg_color = self.translate_color(fg_color)
bg_color = self.translate_color(bg_color)
# Get the pair number for the tuple (fg, bg)
# If it does not exist, create it and give a new unique id.
if (fg_color, bg_color) in self._color_pairs:
pair_nb = self._color_pairs[(fg_color, bg_color)]
else:
pair_nb = len(self._color_pairs)
self.init_pair(pair_nb, fg_color, bg_color)
self._color_pairs[(fg_color, bg_color)] = pair_nb
# Compute curses attributes from the parameters
attr = self.color_pair(pair_nb)
attr |= curses.A_ALTCHARSET if altcharset else 0
attr |= curses.A_BLINK if blink else 0
attr |= curses.A_BOLD if bold else 0
attr |= curses.A_DIM if dim else 0
attr |= curses.A_INVIS if invis else 0
attr |= curses.A_ITALIC if italic else 0
attr |= curses.A_NORMAL if normal else 0
attr |= curses.A_PROTECT if protect else 0
attr |= curses.A_REVERSE if reverse else 0
attr |= curses.A_STANDOUT if standout else 0
attr |= curses.A_UNDERLINE if underline else 0
attr |= curses.A_HORIZONTAL if horizontal else 0
attr |= curses.A_LEFT if left else 0
attr |= curses.A_LOW if low else 0
attr |= curses.A_RIGHT if right else 0
attr |= curses.A_TOP if top else 0
attr |= curses.A_VERTICAL if vertical else 0
attr |= curses.A_CHARTEXT if chartext else 0
return pad.addstr(y, x, msg, attr)
def init_pair(self, number: int, foreground: int, background: int) -> None: def init_pair(self, number: int, foreground: int, background: int) -> None:
return curses.init_pair(number, foreground, background) \ return curses.init_pair(number, foreground, background) \
@ -48,6 +120,10 @@ class Display:
def color_pair(self, number: int) -> int: def color_pair(self, number: int) -> int:
return curses.color_pair(number) if self.screen else 0 return curses.color_pair(number) if self.screen else 0
def init_color(self, number: int, red: int, green: int, blue: int) -> None:
return curses.init_color(number, red, green, blue) \
if self.screen else None
def resize(self, y: int, x: int, height: int, width: int, def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None: resize_pad: bool = True) -> None:
self.x = x self.x = x
@ -152,17 +228,13 @@ class Box(Display):
self.pad = self.newpad(self.rows, self.cols) self.pad = self.newpad(self.rows, self.cols)
self.fg_border_color = fg_border_color or curses.COLOR_WHITE self.fg_border_color = fg_border_color or curses.COLOR_WHITE
pair_number = 4 + self.fg_border_color
self.init_pair(pair_number, self.fg_border_color, curses.COLOR_BLACK)
self.pair = self.color_pair(pair_number)
def display(self) -> None: def display(self) -> None:
self.addstr(self.pad, 0, 0, "" + "" * (self.width - 2) + "", self.addstr(self.pad, 0, 0, "" + "" * (self.width - 2) + "",
self.pair) self.fg_border_color)
for i in range(1, self.height - 1): for i in range(1, self.height - 1):
self.addstr(self.pad, i, 0, "", self.pair) self.addstr(self.pad, i, 0, "", self.fg_border_color)
self.addstr(self.pad, i, self.width - 1, "", self.pair) self.addstr(self.pad, i, self.width - 1, "", self.fg_border_color)
self.addstr(self.pad, self.height - 1, 0, self.addstr(self.pad, self.height - 1, 0,
"" + "" * (self.width - 2) + "", self.pair) "" + "" * (self.width - 2) + "", self.fg_border_color)
self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x + self.width - 1) self.y + self.height - 1, self.x + self.width - 1)

View File

@ -15,15 +15,14 @@ class MapDisplay(Display):
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1) self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
def update_pad(self) -> None: def update_pad(self) -> None:
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack), self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.color_pair(1)) self.pack.tile_fg_color, self.pack.tile_bg_color)
for e in self.map.entities: for e in self.map.entities:
self.addstr(self.pad, e.y, self.pack.tile_width * e.x, self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2)) self.pack[e.name.upper()],
self.pack.entity_fg_color, self.pack.entity_bg_color)
# Display Path map for deubg purposes # Display Path map for debug purposes
# from squirrelbattle.entities.player import Player # from squirrelbattle.entities.player import Player
# players = [ p for p in self.map.entities if isinstance(p,Player) ] # players = [ p for p in self.map.entities if isinstance(p,Player) ]
# player = players[0] if len(players) > 0 else None # player = players[0] if len(players) > 0 else None
@ -42,7 +41,8 @@ class MapDisplay(Display):
# else: # else:
# character = '←' # character = '←'
# self.addstr(self.pad, y, self.pack.tile_width * x, # self.addstr(self.pad, y, self.pack.tile_width * x,
# character, self.color_pair(1)) # character, self.pack.tile_fg_color,
# self.pack.tile_bg_color)
def display(self) -> None: def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx y, x = self.map.currenty, self.pack.tile_width * self.map.currentx

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from random import randint
from typing import List from typing import List
from squirrelbattle.menus import Menu, MainMenu from squirrelbattle.menus import Menu, MainMenu
@ -30,9 +31,9 @@ class MenuDisplay(Display):
def update_pad(self) -> None: def update_pad(self) -> None:
for i in range(self.trueheight): for i in range(self.trueheight):
self.addstr(self.pad, i, 0, " " + self.values[i]) self.addstr(self.pad, i, 0, " " + self.values[i])
# set a marker on the selected line # set a marker on the selected line
self.addstr(self.pad, self.menu.position, 0, ">") self.addstr(self.pad, self.menu.position, 0, " >")
def display(self) -> None: def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \ cornery = 0 if self.height - 2 >= self.menu.position - 1 \
@ -43,7 +44,7 @@ class MenuDisplay(Display):
self.menubox.refresh(self.y, self.x, self.height, self.width) self.menubox.refresh(self.y, self.x, self.height, self.width)
self.pad.erase() self.pad.erase()
self.update_pad() self.update_pad()
self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2, self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 1,
self.height - 2 + self.y, self.height - 2 + self.y,
self.width - 2 + self.x) self.width - 2 + self.x)
@ -102,13 +103,16 @@ class MainMenuDisplay(Display):
self.pad = self.newpad(max(self.rows, len(self.title) + 30), self.pad = self.newpad(max(self.rows, len(self.title) + 30),
max(len(self.title[0]) + 5, self.cols)) max(len(self.title[0]) + 5, self.cols))
self.fg_color = curses.COLOR_WHITE
self.menudisplay = MenuDisplay(self.screen, self.pack) self.menudisplay = MenuDisplay(self.screen, self.pack)
self.menudisplay.update_menu(self.menu) self.menudisplay.update_menu(self.menu)
def display(self) -> None: def display(self) -> None:
for i in range(len(self.title)): for i in range(len(self.title)):
self.addstr(self.pad, 4 + i, max(self.width // 2 self.addstr(self.pad, 4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i]) - len(self.title[0]) // 2 - 1, 0), self.title[i],
self.fg_color)
self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1, self.height + self.y - 1,
self.width + self.x - 1) self.width + self.x - 1)
@ -126,13 +130,16 @@ class MainMenuDisplay(Display):
if menuy <= y < menuy + menuheight and menux <= x < menux + menuwidth: if menuy <= y < menuy + menuheight and menux <= x < menux + menuwidth:
self.menudisplay.handle_click(y - menuy, x - menux, game) self.menudisplay.handle_click(y - menuy, x - menux, game)
if y <= len(self.title):
self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000)
class PlayerInventoryDisplay(MenuDisplay): class PlayerInventoryDisplay(MenuDisplay):
message = _("== INVENTORY ==") message = _("== INVENTORY ==")
def update_pad(self) -> None: def update_pad(self) -> None:
self.addstr(self.pad, 0, (self.width - len(self.message)) // 2, self.addstr(self.pad, 0, (self.width - len(self.message)) // 2,
self.message, curses.A_BOLD | curses.A_ITALIC) self.message, bold=True, italic=True)
for i, item in enumerate(self.menu.values): for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()] 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 else f" {rep} "
@ -160,7 +167,7 @@ class StoreInventoryDisplay(MenuDisplay):
def update_pad(self) -> None: def update_pad(self) -> None:
self.addstr(self.pad, 0, (self.width - len(self.message)) // 2, self.addstr(self.pad, 0, (self.width - len(self.message)) // 2,
self.message, curses.A_BOLD | curses.A_ITALIC) self.message, bold=True, italic=True)
for i, item in enumerate(self.menu.values): for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()] 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 else f" {rep} "

View File

@ -25,7 +25,7 @@ class MessageDisplay(Display):
self.height + 2, self.width + 4) self.height + 2, self.width + 4)
self.box.display() self.box.display()
self.pad.erase() self.pad.erase()
self.addstr(self.pad, 0, 0, self.message, curses.A_BOLD) self.addstr(self.pad, 0, 0, self.message, bold=True)
self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1, self.height + self.y - 1,
self.width + self.x - 1) self.width + self.x - 1)

View File

@ -14,7 +14,6 @@ class StatsDisplay(Display):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols) self.pad = self.newpad(self.rows, self.cols)
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
def update_player(self, p: Player) -> None: def update_player(self, p: Player) -> None:
self.player = p self.player = p
@ -50,9 +49,8 @@ class StatsDisplay(Display):
f"x{self.player.hazel}") f"x{self.player.hazel}")
if self.player.dead: if self.player.dead:
self.addstr(self.pad, 11, 0, _("YOU ARE DEAD"), self.addstr(self.pad, 11, 0, _("YOU ARE DEAD"), curses.COLOR_RED,
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT bold=True, blink=True, standout=True)
| self.color_pair(3))
def display(self) -> None: def display(self) -> None:
self.pad.erase() self.pad.erase()

View File

@ -236,6 +236,9 @@ class TestGame(unittest.TestCase):
""" """
self.game.state = GameMode.MAINMENU self.game.state = GameMode.MAINMENU
# Change the color of the artwork
self.game.display_actions(DisplayActions.MOUSE, 0, 10)
# Settings menu # Settings menu
self.game.display_actions(DisplayActions.MOUSE, 25, 21) self.game.display_actions(DisplayActions.MOUSE, 25, 21)
self.assertEqual(self.game.main_menu.position, 4) self.assertEqual(self.game.main_menu.position, 4)
@ -537,6 +540,8 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.UP) self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.store_menu.position, 1) self.assertEqual(self.game.store_menu.position, 1)
self.game.player.hazel = 0x7ffff42ff
# The second item is not a heart # The second item is not a heart
merchant.inventory[1] = Sword() merchant.inventory[1] = Sword()
# Buy the second item by clicking on it # Buy the second item by clicking on it