Source code for encyclopaedia.book.book_ren

from typing import TYPE_CHECKING

from renpy import store
from renpy.game import persistent

from ..constants_ren import Direction
from ..eventemitter_ren import EventEmitter
from ..exceptions_ren import AddEntryError, GetEntryError
from .actions_ren import BookNextPage, BookPreviousPage

if TYPE_CHECKING:  # pragma: no cover
    from ..encentry_ren import EncEntry
    from ..encyclopaedia_ren import Encyclopaedia

"""renpy
init -84 python:
"""
from math import floor  # NOQA E402
from operator import attrgetter  # NOQA E402
from typing import Any, Callable, Optional, Union  # NOQA E402


[docs] class Book(EventEmitter, store.object): """Container for EncEntry which have a strong relationship with each other. A Book should be placed inside an Encyclopaedia. When sorted and/or filtered, the Book's attributes will be used. Args: number: The Book's number. parent: The parent container for the Book. title: A name for the Book. subject: The subject to associate the Book with. locked: The initial locked status of the Book. locked_persistent: Use persistent data for recording locked status. locked_title: Placeholder text for the title. Shown when the Book is locked. """ def __init__( self, number: Optional[int] = None, parent: Optional['Encyclopaedia'] = None, title: str = "", subject: str = "", locked: bool = False, locked_persistent: Optional[bool] = False, locked_title: str = "???", ) -> None: self.parent: Optional['Encyclopaedia'] = None self.number = number self.subject = subject self.locked_title = locked_title self._title = title self._locked = locked # When locked status is persistent, get the value from renpy.persistent. self.locked_persistent = locked_persistent if self.locked_persistent: persistent_key = self._get_persistent_name() self._locked = getattr(persistent, persistent_key) # persistent variables default to None when not found. if self._locked is None: self._locked = locked setattr(persistent, persistent_key, locked) if parent is not None: parent.add_entry(self) self.pages: list['EncEntry'] = [] self.unlocked_pages: list['EncEntry'] = [] # Cache the current list index of the active page. self._unlocked_page_index: int = 0 self.actions = PageActions(self) self.callbacks: dict[str, list[Callable[['EventEmitter'], None]]] = { "unlocked": [], # Run when this Book is unlocked. "entry_unlocked": [], # Run whenever a page is unlocked. } self._word_count = 0 def _get_persistent_name(self) -> str: """Get a persistent key for the Book's persistent data.""" normalized_title = self._title.lower().replace(' ', '_') rv = f"{normalized_title}_locked" return rv def __repr__(self) -> str: # NOQA D105 return f"Book(number={self.number}, title={self.title}, subject={self.subject})" def __str__(self) -> str: # NOQA D105 return f"{self.title}" 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 title(self) -> str: """The title of the Book.""" return self.__get_entry_data(self._title, self.locked_title) @property def name(self) -> str: """Alias for title, used for sorting in an Encyclopaedia.""" return self.__get_entry_data(self._title, self.locked_title) @property def locked(self) -> bool: """The locked status of the Book.""" return self._locked @locked.setter def locked(self, new_value: bool) -> None: if self.locked_persistent: persistent_key = self._get_persistent_name() setattr(persistent, persistent_key, new_value) # Only run if the Book was locked. if (self._locked) and (new_value is False): if self.parent is not None: self.parent._add_entry_to_unlocked_entries(self) self.parent.emit("entry_unlocked") self.emit("unlocked") self._locked = new_value @property def viewed(self) -> bool: """Determine if the Book has been viewed or not. Return: False if any page in the Book has not been viewed, else True. """ for page in self.pages: if not page.viewed: return False return True @property def active(self) -> 'EncEntry': """Get the object for the currently active page. Raises: GetEntryError: If no active page could be found. """ try: return self.pages[self._unlocked_page_index] except IndexError: page_num = len(self.pages) if page_num == 0: raise GetEntryError("Book has no pages.") from IndexError else: # This will only get triggered if the dev is messing with private variables. raise GetEntryError( ( "Tried to fetch page at index: " f"<{self._unlocked_page_index}>. " f"Maximum value is: <{page_num - 1}>" ), ) from IndexError @property def current_page(self) -> 'EncEntry': """Alias for active. Used by screens.""" return self.active @property def percentage_unlocked(self) -> float: """Get the percentage of the Book's pages which are unlocked. Return: Number between 0.0 and 1.0 Raises: ZeroDivisionError: If the Book is empty """ float_size = len(self.unlocked_pages) float_size_all = len(self.pages) try: amount_unlocked = float_size / float_size_all except ZeroDivisionError as err: raise ZeroDivisionError( 'Cannot calculate percentage unlocked of empty Book', ) from err return amount_unlocked @property def word_count(self) -> int: """Get the word count for the entire Book. All the text from all the pages in the book will be counted. Return: The number of words in the Book. """ return self._word_count def _recalculate_word_count(self) -> int: """Recalculate the word count of the book. This is used so we don't have to recalculate the word count every time it's checked. We only run this when an entry is added. Returns: The new word count """ count = 0 for page in self.pages: count += page.word_count self._word_count = count return count
[docs] def add_entry(self, entry: 'EncEntry') -> bool: """Add an EncEntry to this Book. Returns: True if the operation was a success Raises: AddEntryError """ if entry.parent is not None: if entry.parent == self: raise AddEntryError( f"<{entry}> is already a page of <{self}>", ) else: raise AddEntryError( f"<{entry}> already has a parent: <{entry.parent}>", ) if entry.number is None: raise AddEntryError(f"<{entry} does not have a number.") if any(i for i in self.pages if i.number == entry.number): raise AddEntryError( f"{entry.number} is already taken.", ) if entry.subject: raise AddEntryError( "Entries inside a Book cannot have their own subject.", ) entry.parent = self entry.subject = self.subject entry.page_number = entry.number + 1 self.pages.append(entry) self.pages = sorted( self.pages, key=attrgetter('number'), ) if entry.locked is False: self._add_entry_to_unlocked_entries(entry) self._recalculate_word_count() return True
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) self.unlocked_pages = sorted( self.unlocked_pages, key=attrgetter('number'), )
[docs] def set_active_page(self, page_number: int) -> None: """Set a page to be active, based on the page number. Arguments: page_number: The number of the page to set. """ if page_number < 0: raise ValueError("Invalid page number.") if page_number > len(self.pages): raise ValueError("Invalid page number.") self._unlocked_page_index = page_number
def _change_page(self, direction: Direction) -> bool: """Change the current active 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.active.viewed is False: self.active.viewed = True return True
[docs] def previous_page(self) -> bool: """Set the previous page as the current page.""" return self._change_page(Direction.BACKWARD)
[docs] def next_page(self) -> bool: """Set the next page as the current page.""" return self._change_page(Direction.FORWARD)
class PageActions: """Actions used to navigate a Book.""" def __init__(self, parent: 'Book') -> None: self.parent = parent def PreviousPage(self) -> BookPreviousPage: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return BookPreviousPage(book=self.parent) def NextPage(self) -> BookNextPage: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return BookNextPage(book=self.parent)