from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import pygame
from pygame_gui.core import ObjectID
from pygame_gui.core import UIElement as PygameUiElement
from pygame_gui.elements import UIImage, UITextBox, UIVerticalScrollBar
from snecs.typedefs import EntityID
import scripts.engine.core.matter
from scripts.engine.core import utility, world
from scripts.engine.core.component import Aesthetic, Afflictions, CombatStats, Identity, Resources, Traits
from scripts.engine.core.utility import get_class_members
from scripts.engine.internal.constant import (
ASSET_PATH,
GAP_SIZE,
ICON_SIZE,
INFINITE,
PrimaryStat,
SecondaryStat,
UIElement,
)
from scripts.engine.internal.event import event_hub, ExitMenuEvent
from scripts.engine.widgets.window import Window
if TYPE_CHECKING:
from typing import List, Optional, Tuple, Union
from pygame_gui import UIManager
[docs]class ActorInfo(Window):
"""
Full detail about an npc entity.
"""
[docs] def __init__(self, rect: pygame.Rect, manager: UIManager):
# sections
self.selected_entity: Optional[EntityID] = None
self.sections: List[PygameUiElement] = []
self.section_base_positions: List[int] = []
# complete base class init
super().__init__(rect, manager, object_id=ObjectID("#actor_info", "@menu_window"))
# setup scroll bar
self.scrollbar_width = 50
self.scrollbar_size = 0.3
self.scrollbar = UIVerticalScrollBar(
pygame.Rect((rect.width - self.scrollbar_width, 0), (self.scrollbar_width, rect.height)),
self.scrollbar_size,
manager,
self,
self,
"#scrollbar",
)
# block mouse clicks outside of menu
self.set_blocking(True)
# show self
self.show()
# confirm init complete
logging.debug(f"Actor Info initialised.")
############## GET / SET ########################
[docs] def set_entity(self, entity: EntityID):
"""
Set the selected entity to show the info for that entity.
"""
self.selected_entity = entity
############### ACTIONS #########################
def _shift_children(self, target_y):
for i, child in enumerate(self.sections):
child.set_relative_position((child.relative_rect.x, self.section_base_positions[i] - target_y))
[docs] def update(self, time_delta: float):
super().update(time_delta)
new_y_portion = self.scrollbar.scroll_position / self.scrollbar.bottom_limit / (1 - self.scrollbar_size)
if self.sections != []:
if (
self.section_base_positions[-1] + self.sections[-1].relative_rect.height
> self.rect.height - self.section_base_positions[0]
):
new_y = new_y_portion * (
self.section_base_positions[-1]
+ self.sections[-1].relative_rect.height
- (self.rect.height - self.section_base_positions[0])
)
self._shift_children(new_y)
[docs] def show(self):
"""
Show the entity info. Builds the sections required, after clearing any existing.
"""
super().show()
self.visible = True
entity = self.selected_entity
# clear to refresh first
self.cleanse()
if entity:
info: List[Tuple[str, Union[str, pygame.Surface]]] = []
section_break_image = utility.get_image(
ASSET_PATH / "ui/menu_window_n_repeat.png", (self.rect.width - self.scrollbar_width, 13)
)
# get aesthetic
aesthetic = scripts.engine.core.matter.get_entitys_component(entity, Aesthetic)
info.append(("image", aesthetic.sprites.icon))
# get identity
identity = scripts.engine.core.matter.get_entitys_component(entity, Identity)
info.append(("text", identity.name))
# get resources
resources = scripts.engine.core.matter.get_entitys_component(entity, Resources)
if resources:
info.append(("text", f"Health: {resources.health}"))
info.append(("text", f"Stamina: {resources.stamina}"))
info.append(("image", section_break_image))
# get stats
stats = scripts.engine.core.matter.get_entitys_component(entity, CombatStats)
if stats:
primary_stats = utility.get_class_members(PrimaryStat)
for name in primary_stats:
try:
stat_value = getattr(stats, name.lower())
name = name.title()
name = name.replace("_", " ")
info.append(("text", f"{name}: {stat_value}"))
# in case it fails to pull expected attribute
except AttributeError:
logging.warning(f"ActorInfo: attribute {name} not found in primary stats.")
secondary_stats = get_class_members(SecondaryStat)
for name in secondary_stats:
try:
stat_value = getattr(stats, name.lower())
name = name.title()
name = name.replace("_", " ")
info.append(("text", f"{name}: {stat_value}"))
# in case it fails to pull expected attribute
except AttributeError:
logging.warning(f"ActorInfo: attribute {name} not found in secondary stats.")
info.append(("image", section_break_image))
# get traits (skip for projectiles)
try:
traits = scripts.engine.core.matter.get_entitys_component(entity, Traits)
if traits:
names = ""
for name in traits.names:
# if more than one trait add a separator
if len(names) > 1:
names += ", "
names += f"{name}"
info.append(("text", names))
info.append(("image", section_break_image))
except:
pass
# get afflictions
afflictions = scripts.engine.core.matter.get_entitys_component(entity, Afflictions)
if afflictions:
for affliction in afflictions.active:
# get duration
if affliction.duration == INFINITE:
duration = "∞"
else:
duration = affliction.duration
affliction_icon = utility.get_image(affliction.icon_path, (32, 32))
info.append(("image", affliction_icon))
info.append(("text", f"{affliction.key.title()}: {duration}"))
# if no afflictions, say so
if not afflictions:
info.append(("text", "Not afflicted."))
# create the box for the info
self._create_sections(info)
# refresh scrollbar
self.scrollbar.redraw_scrollbar()
[docs] def cleanse(self):
"""
Cleanse existing section info.
"""
# kill the box and clear the reference
for element in self.sections:
element.kill()
self.sections = []
############## CREATE ########################
def _create_sections(self, info: List[Tuple[str, Union[str, pygame.Surface]]]):
"""
Create sections for the information about the tile
"""
sections = []
section_base_positions = []
current_y = 0
current_text_block = ""
# draw info
x = 0
width = self.rect.width - self.scrollbar_width
text_height = 0 # box will resize height anyway
# loop each image provided and use as header for each group of info
for type_str, text_or_image in info:
# build current text block
if type_str == "text":
assert isinstance(text_or_image, str) # handle mypy error
current_text_block += text_or_image + "<br>"
elif type_str == "image":
assert isinstance(text_or_image, pygame.Surface) # handle mypy error
# if we have text in the previous block, show it
if current_text_block:
## Display text
rect = pygame.Rect((x, current_y), (width, text_height))
ui_text = UITextBox(
html_text=current_text_block,
relative_rect=rect,
manager=self.ui_manager,
wrap_to_height=True,
layer_starting_height=1,
container=self.get_container(),
)
sections.append(ui_text)
section_base_positions.append(ui_text.relative_rect.y)
# update position
current_y += ui_text.rect.height + GAP_SIZE
# clear to prevent any carry over
ui_text = None
current_text_block = ""
## Display image
# draw info
image_width = text_or_image.get_width()
image_height = text_or_image.get_height()
# if image is the icon_path then draw centre, otherwise draw left
if image_width == ICON_SIZE:
draw_x = int((self.rect.width / 2) - (image_width / 2))
else:
draw_x = 0
# create rect and image element
image_rect = pygame.Rect((draw_x, current_y), (image_width, image_height))
ui_image = UIImage(
relative_rect=image_rect,
image_surface=text_or_image,
manager=self.ui_manager,
container=self.get_container(),
)
sections.append(ui_image)
section_base_positions.append(ui_image.relative_rect.y)
# update position
current_y += image_height + GAP_SIZE
# clear to prevent any carry over
ui_image = None
# we've left the loop, clean up left over text
if current_text_block:
## Display text
rect = pygame.Rect((x, current_y), (width, text_height))
ui_text = UITextBox(
html_text=current_text_block,
relative_rect=rect,
manager=self.ui_manager,
wrap_to_height=True,
layer_starting_height=1,
container=self.get_container(),
)
sections.append(ui_text)
section_base_positions.append(ui_text.relative_rect.y)
# update main sections list
self.sections = sections
self.section_base_positions = section_base_positions