Merge branch 'change_image_color' into 'master'
Better color support Closes #43 See merge request ynerant/squirrel-battle!46
This commit is contained in:
commit
1986630da1
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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} "
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue