from typing import TYPE_CHECKING
from renpy import store
from renpy.color import Color
from renpy.display.transform import Transform
from renpy.game import persistent
from renpy.store import TintMatrix
from .utils_ren import string_to_list
from .eventemitter_ren import EventEmitter
from .constants_ren import Direction
if TYPE_CHECKING: # pragma: no cover
from .encyclopaedia_ren import Encyclopaedia
"""renpy
init python:
"""
from operator import attrgetter # NOQA E402
from typing import Any, Callable, Optional, Union # NOQA E402
[docs]
class EncEntry(EventEmitter, store.object):
"""Store an Entry's content.
EncEntry instances should be added to an Encyclopaedia or another EncEntry.
Args:
parent: The parent container for the EncEntry.
number: The entry's number.
If this is not set then it will be given a number automatically.
name: Title, normally used for buttons and headings.
text: The text that will be displayed when the entry is viewed.
subject: The subject to associate the entry with.
Used for sorting and filtering.
viewed: Set the viewed status of the EncEntry. Default is False.
Only use if the Encyclopaedia is save-game independent.
viewed_persistent: Use persistent data for recording viewed status.
locked: Set the locked status of the EncEntry. Default is False.
locked_persistent: Use persistent data for recording locked status.
image: The image displayed with the Entry text. Default is None.
locked_name: Placeholder text for the name. Shown when the entry is locked.
locked_text: Placeholder text for the text. Shown when the entry is locked.
locked_image: Placeholder image for the image. Shown when the entry is locked.
locked_image_tint: If no specific locked image is provided,
a tinted version of the image will be used.
The amount of tinting can be set with RGB values in a tuple.
Attributes:
has_image: True if an image was provided, else False.
pages: List of all the pages this entry contains.
has_pages: If an entry has any sub-entries.
"""
def __init__(self,
parent: Optional[Union['Encyclopaedia', 'EncEntry']] = None,
number: Optional[int] = None,
name: str = "",
text: Union[str, list[str]] = "",
subject: str = "",
viewed: bool = False,
viewed_persistent: Optional[bool] = False,
locked: bool = False,
locked_persistent: Optional[bool] = False,
image: Optional[str] = None,
locked_name: str = "???",
locked_text: str = "???",
locked_image: Optional[str] = None,
locked_image_tint: tuple[float, float, float] = (0.0, 0.0, 0.0),
) -> None:
# Place the entry into the assigned Encyclopaedia or EncEntry.
# self.parent is set to None so that add_entry doesn't think
# this EncEntry is already inside an Encyclopaedia.
self.parent: Optional[Union['Encyclopaedia', 'EncEntry']] = None
# number is used by the Encyclopaedia, page_number by EncEntry.
self.number = number
self.page_number: int = 1
self.locked_name = locked_name
self.locked_text = string_to_list(locked_text)
self.locked_image = locked_image
self._name = name
self._text = string_to_list(text)
self._viewed = viewed
self.subject = subject
self._locked = locked
self._image = image
# When locked status is persistent, get the value from renpy.persistent.
self.locked_persistent = locked_persistent
if self.locked_persistent:
self._locked = getattr(persistent, self._name + "_locked")
# persistent variables default to None when not found.
if self._locked is None:
self._locked = locked
setattr(persistent, self._name + "_locked", locked)
if parent is not None:
parent.add_entry(self)
self.has_image: bool = False
if image is not None:
self.has_image = True
# If there's an image, but no locked image is specified,
# tint the image and use it as the locked image.
if locked_image is None:
c = Color(rgb=locked_image_tint)
self.locked_image = Transform(image, matrixcolor=TintMatrix(c))
# Setup pages
# The current instance must be the first in the sub-entry list.
self.pages: list['EncEntry'] = [self]
# Cache unlocked pages
self.unlocked_pages: list['EncEntry'] = [self]
self.has_pages = False
# Relative to the unlocked pages, cache the position of the active page.
self._unlocked_page_index = 0
self.callbacks: dict[str, list[Callable[['EncEntry'], None]]] = {
"viewed": [], # Run when this entry is viewed for the first time.
"unlocked": [], # Run when this entry is unlocked.
"entry_unlocked": [], # Run whenever a child entry is unlocked.
}
# When viewed status is persistent, get the value from renpy.persistent.
self.viewed_persistent = viewed_persistent
if self.viewed_persistent:
self._viewed = getattr(persistent, self._name + "_viewed")
# persistent variables default to None when not found.
if self._viewed is None:
self.viewed = False
def __repr__(self) -> str: # NOQA D105
return f"EncEntry(number={self.number}, name={self.name})"
def __str__(self) -> str: # NOQA D105
return self.label
@property
def locked(self) -> bool:
"""Determine if the entry's data can be viewed or not.
Changing this variable will modify the entry's locked status.
"""
return self._locked
@locked.setter
def locked(self, new_value: bool) -> None:
if self.locked_persistent:
setattr(persistent, self._name + "_locked", new_value)
# Only run if the entry was locked
if (self._locked is not False) and (new_value is False):
self._locked = new_value
if self.parent is not None:
self.parent.add_entry_to_unlocked_entries(self)
self.parent.emit("entry_unlocked")
self.emit("unlocked")
@property
def viewed(self) -> bool:
"""Determine if the entry has been viewed or not.
Changing this variable will modify the entry's viewed status.
"""
return self._viewed
@viewed.setter
def viewed(self, new_value: bool) -> None:
if self.viewed_persistent:
setattr(persistent, self._name + "_viewed", new_value)
self._viewed = new_value
@property
def label(self) -> str:
"""The number and name of the entry, in the format of 'number: name'."""
number = str(self.number).zfill(2)
return f"{number}: {self.name}"
@property
def current_page(self) -> 'EncEntry':
"""Get the sub-page that's currently viewing viewed.
Setting this attribute should be done using an integer.
"""
return self.unlocked_pages[self._unlocked_page_index]
def __get_entry_data(self, data: Any, locked_data: Any) -> Any:
"""Used by (name, text, image) attributes to check if the placeholder should be returned.
Return:
If True or None, return the data requested,
otherwise the placeholder for the data
"""
if self.locked or self.locked is None:
return locked_data
return data
@property
def name(self) -> str:
"""The name for the entry. Return placeholder when entry is locked."""
return self.__get_entry_data(self._name, self.locked_name)
@name.setter
def name(self, val: str) -> None:
self._name = val
self.viewed = False
@property
def text(self) -> list[str]:
"""The text for the entry. Return placeholder when entry is locked."""
return self.__get_entry_data(self._text, self.locked_text)
@text.setter
def text(self, val: list[str]) -> None:
self._text = val
self.viewed = False
@property
def image(self) -> str:
"""The image for the entry. Return placeholder when entry is locked."""
return self.__get_entry_data(self._image, self.locked_image)
@image.setter
def image(self, val: str) -> None:
self.has_image = True
self._image = val
self.viewed = False
[docs]
def add_entry(self, entry: 'EncEntry') -> bool:
"""Add multiple pages to the entry in the form of sub-entries.
Args:
entry: The entry to add as a sub-entry.
Return:
True if anything was added, else False.
Raise:
AttributeError: If the entry is already the page of another entry.
ValueError: If the entry has a number that is already taken.
"""
if entry.parent is not None and entry.parent != self:
raise ValueError(
f"{entry} is already a page of {self.parent}",
)
# When a new entry has a number, ensure it's not already used.
if entry.number is not None:
if any(i for i in self.pages if i.number == entry.number):
raise ValueError(
f"{entry.number} is already taken.",
)
elif entry.number is None:
entry.number = len(self.pages) + 1
# Child Entries always have matching number and page_number
entry.page_number = entry.number
entry.parent = self
if entry not in self.pages:
if entry.locked is False:
self.add_entry_to_unlocked_entries(entry)
self.pages.append(entry)
# Sort by number.
self.pages = sorted(
self.pages,
key=attrgetter('page_number'),
)
self.has_pages = True
return True
return False
[docs]
def add_entry_to_unlocked_entries(self, entry: 'EncEntry') -> None:
"""Add an entry to the list of unlocked entries.
Args:
entry: The Entry to add to the unlocked entries list.
"""
self.unlocked_pages.append(entry)
# Remove duplicates
self.unlocked_pages = list(set(self.unlocked_pages))
self.unlocked_pages = sorted(
self.unlocked_pages,
key=attrgetter('page_number'),
)
def _change_page(self, direction: Direction) -> bool:
"""Change the current sub-entry page."""
new_page_number = self._unlocked_page_index + direction.value
# Don't allow moving beyond bounds.
if new_page_number < 0:
return False
elif new_page_number > len(self.pages):
return False
self._unlocked_page_index = new_page_number
# Update viewed state
if self.current_page.viewed is False:
self.current_page.viewed = True
return True
[docs]
def previous_page(self) -> bool:
"""Set the previous sub-entry page as the current page."""
return self._change_page(Direction.BACKWARD)
[docs]
def next_page(self) -> bool:
"""Set the next sub-entry page as the current page."""
return self._change_page(Direction.FORWARD)
@property
def word_count(self) -> int:
"""Get the word count for the EncEntry's text.
Return:
The number of words in the EncEntry.
"""
count = 0
for item in self._text:
count += len(item.split())
return count