Move the renderable part of Entity to a Renderable component

Move symbol, render order, foreground, and background properties on Entity
to a new Component called Renderable.
This commit is contained in:
Eryn Wells 2023-03-11 00:58:58 -08:00
parent 01b549bc6e
commit def79386d8
5 changed files with 80 additions and 71 deletions

View file

@ -1,7 +1,8 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
import random import random
from typing import Optional from enum import Enum
from typing import Optional, Tuple
class Component: class Component:
@ -75,3 +76,42 @@ class Fighter(Component):
def _reset_passive_heal_clock(self) -> None: def _reset_passive_heal_clock(self) -> None:
self.__ticks_since_last_passive_heal = 0 self.__ticks_since_last_passive_heal = 0
self.__ticks_for_next_passive_heal = random.randint(30, 70) self.__ticks_for_next_passive_heal = random.randint(30, 70)
class Renderable(Component):
class Order(Enum):
'''
These values indicate the order that an entity with a Renderable
component should be rendered. Higher values are rendered later and
therefore on top of items with lower orderings.
'''
ITEM = 1000
ACTOR = 2000
HERO = 3000
def __init__(
self,
symbol: str,
order: Order = Order.ACTOR,
fg: Optional[Tuple[int, int, int]] = None,
bg: Optional[Tuple[int, int, int]] = None):
if len(symbol) != 1:
raise ValueError(f'Symbol string "{symbol}" must be of length 1')
self.symbol = symbol
'''The symbol that represents this renderable on the map'''
self.order = order
'''
Specifies the layer at which this entity is rendered. Higher values are
rendered later, and thus on top of lower values.
'''
self.foreground = fg
'''The foreground color of the entity'''
self.background = bg
'''The background color of the entity'''
def __repr__(self):
return f'{self.__class__.__name__}("{self.symbol}", {self.order}, {self.foreground}, {self.background})'

View file

@ -165,7 +165,7 @@ class Engine:
if log.ACTIONS_TREE.isEnabledFor(log.INFO) and self.map.visible[tuple(action.actor.position)]: if log.ACTIONS_TREE.isEnabledFor(log.INFO) and self.map.visible[tuple(action.actor.position)]:
if result.alternate: if result.alternate:
alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor.symbol}]' alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor}]'
else: else:
alternate_string = str(result.alternate) alternate_string = str(result.alternate)
log.ACTIONS_TREE.info( log.ACTIONS_TREE.info(

View file

@ -45,7 +45,8 @@ class Interface:
hero = self.engine.hero hero = self.engine.hero
self.info_window.update_hero(hero) self.info_window.update_hero(hero)
sorted_entities = sorted(self.engine.entities, key=lambda e: e.render_order.value) sorted_entities = sorted(filter(lambda e: e.renderable is not None, self.engine.entities),
key=lambda e: e.renderable.order.value)
self.map_window.entities = sorted_entities self.map_window.entities = sorted_entities
def draw(self): def draw(self):

View file

@ -190,6 +190,10 @@ class MapWindow(Window):
if not self.map.point_is_visible(entity_position): if not self.map.point_is_visible(entity_position):
continue continue
renderable = ent.renderable
if not renderable:
continue
# Entity positions are relative to the (0, 0) point of the Map. In # Entity positions are relative to the (0, 0) point of the Map. In
# order to render them in the correct position in the console, we # order to render them in the correct position in the console, we
# need to transform them into viewport-relative coordinates. # need to transform them into viewport-relative coordinates.
@ -200,6 +204,6 @@ class MapWindow(Window):
console.print( console.print(
x=position.x, x=position.x,
y=position.y, y=position.y,
string=ent.symbol, string=renderable.symbol,
fg=ent.foreground, fg=renderable.foreground,
bg=tuple(map_tile_at_entity_position['bg'][:3])) bg=tuple(map_tile_at_entity_position['bg'][:3]))

View file

@ -2,13 +2,12 @@
'''Defines a number of high-level game objects. The parent class of all game objects is the Entity class.''' '''Defines a number of high-level game objects. The parent class of all game objects is the Entity class.'''
from enum import Enum from typing import TYPE_CHECKING, Optional, Type
from typing import TYPE_CHECKING, Optional, Tuple, Type
import tcod import tcod
from . import items from . import items
from .components import Fighter from .components import Fighter, Renderable
from .geometry import Point from .geometry import Point
from .monsters import Species from .monsters import Species
@ -16,16 +15,6 @@ if TYPE_CHECKING:
from .ai import AI from .ai import AI
class RenderOrder(Enum):
'''
These values indicate the order that an Entity should be rendered. Higher values are rendered later and therefore on
top of items with lower orderings.
'''
ITEM = 1000
ACTOR = 2000
HERO = 3000
class Entity: class Entity:
'''A single-tile drawable entity with a symbol and position '''A single-tile drawable entity with a symbol and position
@ -36,19 +25,9 @@ class Entity:
game game
position : Point position : Point
The Entity's location on the map The Entity's location on the map
foreground : Tuple[int, int, int]
The foreground color used to render this Entity
background : Tuple[int, int, int], optional
The background color used to render this Entity
symbol : str
A single character string that represents this character on the map
blocks_movement : bool blocks_movement : bool
True if this Entity blocks other Entities from moving through its True if this Entity blocks other Entities from moving through its
position position
render_order : RenderOrder
One of the RenderOrder values that specifies a layer at which this
entity will be rendered. Higher values are rendered on top of lower
values.
''' '''
# A monotonically increasing identifier to help differentiate between # A monotonically increasing identifier to help differentiate between
@ -57,66 +36,51 @@ class Entity:
def __init__( def __init__(
self, self,
symbol: str,
*, *,
position: Optional[Point] = None, position: Optional[Point] = None,
blocks_movement: Optional[bool] = True, blocks_movement: Optional[bool] = True,
render_order: RenderOrder = RenderOrder.ITEM, renderable: Optional[Renderable] = None):
fg: Optional[Tuple[int, int, int]] = None,
bg: Optional[Tuple[int, int, int]] = None):
self.identifier = Entity.__next_identifier self.identifier = Entity.__next_identifier
self.position = position if position else Point() self.position = position if position else Point()
self.foreground = fg if fg else (255, 255, 255) self.renderable = renderable
self.background = bg
self.symbol = symbol
self.blocks_movement = blocks_movement self.blocks_movement = blocks_movement
self.render_order = render_order
Entity.__next_identifier += 1 Entity.__next_identifier += 1
def print_to_console(self, console: tcod.Console) -> None: def __str__(self):
'''Render this Entity to the console''' return f'{self.__class__.__name__}!{self.identifier}'
console.print(x=self.position.x, y=self.position.y, string=self.symbol, fg=self.foreground, bg=self.background)
def __str__(self) -> str: def __repr__(self):
return f'{self.symbol}!{self.identifier} at {self.position}' return f'{self.__class__.__name__}(position={self.position!r}, blocks_movement={self.blocks_movement}, renderable={self.renderable!r})'
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.symbol!r}, position={self.position!r}, fg={self.foreground!r}, bg={self.background!r})'
class Actor(Entity): class Actor(Entity):
''' '''
An actor is an abstract class that defines an object that can act in the game world. Entities that are actors will An actor is an abstract class that defines an object that can act in the
be allowed an opportunity to perform an action during each game turn. game world. Entities that are actors will be allowed an opportunity to
perform an action during each game turn.
### Attributes
Attributes
----------
ai : AI, optional ai : AI, optional
If an entity can act on its own behalf, an instance of an AI class If an entity can act on its own behalf, an instance of an AI class
fighter : Fighter, optional fighter : Fighter, optional
If an entity can fight or take damage, an instance of the Fighter class. This is where hit points, attack power, If an entity can fight or take damage, an instance of the Fighter class.
defense power, etc live. This is where hit points, attack power, defense power, etc live.
''' '''
def __init__( def __init__(
self, self,
symbol: str,
*, *,
position: Optional[Point] = None, position: Optional[Point] = None,
blocks_movement: Optional[bool] = True, blocks_movement: Optional[bool] = True,
render_order: RenderOrder = RenderOrder.ACTOR, renderable: Optional[Renderable] = None,
ai: Optional['AI'] = None, ai: Optional['AI'] = None,
fighter: Optional[Fighter] = None, fighter: Optional[Fighter] = None):
fg: Optional[Tuple[int, int, int]] = None,
bg: Optional[Tuple[int, int, int]] = None):
super().__init__( super().__init__(
symbol,
position=position, position=position,
blocks_movement=blocks_movement, blocks_movement=blocks_movement,
fg=fg, renderable=renderable)
bg=bg,
render_order=render_order)
# Components # Components
self.ai = ai self.ai = ai
@ -138,7 +102,7 @@ class Actor(Entity):
return False return False
def __repr__(self) -> str: def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.symbol!r}, position={self.position!r}, fighter={self.fighter!r}, ai={self.ai!r}, fg={self.foreground!r}, bg={self.background!r})' return f'{self.__class__.__name__}(position={self.position!r}, fighter={self.fighter!r}, ai={self.ai!r}, renderable={self.renderable!r})'
class Hero(Actor): class Hero(Actor):
@ -146,11 +110,9 @@ class Hero(Actor):
def __init__(self, position: Point): def __init__(self, position: Point):
super().__init__( super().__init__(
'@',
position=position, position=position,
fighter=Fighter(maximum_hit_points=30, attack_power=5, defense=2), fighter=Fighter(maximum_hit_points=30, attack_power=5, defense=2),
render_order=RenderOrder.HERO, renderable=Renderable('@', Renderable.Order.HERO, tuple(tcod.white)))
fg=tuple(tcod.white))
@property @property
def name(self) -> str: def name(self) -> str:
@ -176,12 +138,13 @@ class Monster(Actor):
defense=species.defense) defense=species.defense)
super().__init__( super().__init__(
species.symbol,
ai=ai_class(self), ai=ai_class(self),
position=position, position=position,
fighter=fighter, fighter=fighter,
fg=species.foreground_color, renderable=Renderable(
bg=species.background_color) symbol=species.symbol,
fg=species.foreground_color,
bg=species.background_color))
self.species = species self.species = species
@ -206,12 +169,13 @@ class Item(Entity):
'''An instance of an Item''' '''An instance of an Item'''
def __init__(self, kind: items.Item, position: Optional[Point] = None, name: Optional[str] = None): def __init__(self, kind: items.Item, position: Optional[Point] = None, name: Optional[str] = None):
super().__init__(kind.symbol, super().__init__(position=position,
position=position,
blocks_movement=False, blocks_movement=False,
render_order=RenderOrder.ITEM, renderable=Renderable(
fg=kind.foreground_color, symbol=kind.symbol,
bg=kind.background_color) order=Renderable.Order.ITEM,
fg=kind.foreground_color,
bg=kind.background_color))
self.kind = kind self.kind = kind
self._name = name self._name = name