Source code for encyclopaedia.encyclopaedia_ren

from typing import TYPE_CHECKING

from renpy import store

from .actions_ren import (
    ClearFilter,
    CloseActiveEntry,
    FilterBySubject,
    NextEntry,
    NextPage,
    PreviousEntry,
    PreviousPage,
    ResetSubPage,
    SetEntry,
    SortEncyclopaedia,
    ToggleShowLockedButtonsAction,
    ToggleShowLockedEntryAction,
)
from .book import Book
from .constants_ren import Direction, SortMode
from .entry_sorting_ren import push_locked_to_bottom
from .eventemitter_ren import EventEmitter
from .exceptions_ren import AddEntryError, UnknownEntryError
from .types_ren import ENTRY_TYPE

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

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


[docs] class Encyclopaedia(EventEmitter, store.object): """Container that manages the behaviour of a collection of EncEntry objects. Args: sorting_mode: How entries are sorted. Default is SortMode.NUMBER. show_locked_buttons: If True, locked entries show a placeholder label on the listing screen. show_locked_entry: If True, locked entries can be viewed, but the data is hidden from view with a placeholder. list_screen: The Ren'Py screen to display the list of entries. entry_screen: The Ren'Py screen to display an open entry. hyperlink_screen: The Ren'Py screen to display an entry opened from a hyperlink. name: A optional name for the Encyclopaedia. Attributes: all_entries: All entries, regardless of status. unlocked_entries: Only unlocked entries. filtered_entries: Entries that match a subject filter. filtering: The subject that's being used as a filter. size_all: Length of self.all_entries. size_unlocked: Length of self.unlocked_entries. reverse_sorting: Should sorting occur in reverse or not. nest_alphabetical_sort: Should alphabetical sorting display each letter as a subject. current_position: Index for the current entry open. subjects: Collection of every subject. active: The currently open entry. locked_at_bottom: True if locked entries should appear at the bottom of the entry list or not. """ def __init__(self, sorting_mode: SortMode = SortMode.NUMBER, show_locked_buttons: bool = False, show_locked_entry: bool = False, list_screen: str = 'encyclopaedia_list', entry_screen: str = 'encyclopaedia_entry', hyperlink_screen: str = 'encyclopaedia_list', name: str = '', ) -> None: self._sorting_mode = sorting_mode self._reverse_sorting: bool = False self.show_locked_buttons = show_locked_buttons self.show_locked_entry = show_locked_entry self.list_screen = list_screen self.entry_screen = entry_screen self.hyperlink_screen = hyperlink_screen self.name = name self.all_entries: list[ENTRY_TYPE] = [] self.unlocked_entries: list[ENTRY_TYPE] = [] self.filtered_entries: list[ENTRY_TYPE] = [] self.filtering: Union[bool, str] = False self.nest_alphabetical_sort: bool = True self.current_position: int = 0 self.subjects: list[str] = [] self.active: Optional[ENTRY_TYPE] = None self._current_entries = self.all_entries self.locked_at_bottom: bool = True self.callbacks: dict[str, list[Callable[['EventEmitter'], None]]] = { "entry_unlocked": [], # Run whenever a child entry is unlocked. } def __repr__(self) -> str: # NOQA D105 return f"Encyclopaedia(name={self.name}, length={len(self.all_entries)})" def __str__(self) -> str: # NOQA D105 return f"Encyclopaedia: {self.name}" def __len__(self) -> int: """The total number of entries, relative to if locked ones are shown or not.""" rv = len(self.viewable_entries) return rv @property def viewable_entries(self) -> list[ENTRY_TYPE]: """Get the list of entries which are currently viewable.""" if self.show_locked_entry: return self.all_entries else: return self.unlocked_entries @property def current_entries(self) -> list[ENTRY_TYPE]: """Get all the entries which should be visible to the user. Return: List of EncEntries. If filtering, only entries that match the filter are returned. """ rv = self.unlocked_entries if self.filtering: rv = self.filtered_entries elif self.show_locked_buttons: rv = self.all_entries return rv @property def current_entry(self) -> ENTRY_TYPE: """Get the entry at current_position. If show locked entries, pull from all_entries. Else, pull from unlocked_entries. Return: EncEntry """ entry = self.viewable_entries[self.current_position] return entry @property def percentage_unlocked(self) -> float: """Get the percentage of the Encyclopaedia that's unlocked. Return: Number between 0.0 and 1.0 Raises: ZeroDivisionError: If the Encyclopaedia is empty """ float_size = len(self.unlocked_entries) float_size_all = len(self.all_entries) try: amount_unlocked = float_size / float_size_all except ZeroDivisionError as err: raise ZeroDivisionError( 'Cannot calculate percentage unlocked of empty Encyclopaedia', ) from err percentage = floor(amount_unlocked * 100) return percentage @property def sorting_mode(self) -> SortMode: """Get the current sorting mode of the Encyclopaedia.""" return self._sorting_mode @property def reverse_sorting(self) -> bool: """Get the direction of the current sorting. Returns: True if sorting is reversed, else False. """ return self._reverse_sorting def _sort_entries(self, entries: list[ENTRY_TYPE]) -> None: """Sort an entry list. The Encyclopaedia's `sorting_mode` and `reverse_sorting` attributes are used for sorting. Args: entries: The EncEntry list to sort. """ sorting_mode = self.sorting_mode reverse = self.reverse_sorting # The reverse of SortMode.ALPHABETICAL is the equivalent of # SortMode.REVERSE_ALPHABETICAL and vice versa. reverse_alphabetical = False if (sorting_mode == SortMode.REVERSE_ALPHABETICAL) and not reverse: reverse_alphabetical = True elif (sorting_mode == SortMode.ALPHABETICAL) and reverse: reverse_alphabetical = True if sorting_mode == SortMode.NUMBER: entries.sort(key=attrgetter('number')) else: # If not number, always sort by name first. entries.sort(reverse=reverse_alphabetical, key=attrgetter('name')) if sorting_mode == SortMode.UNREAD: entries.sort(reverse=reverse, key=attrgetter('viewed')) elif sorting_mode == SortMode.SUBJECT: entries.sort(reverse=reverse, key=attrgetter('subject')) if self.locked_at_bottom: push_locked_to_bottom(entries) def _add_entry_to_unlocked_entries(self, entry: ENTRY_TYPE) -> None: """Add an entry to the list of unlocked entries. Args: entry: The Entry to add to the unlocked entries list. """ self.unlocked_entries.append(entry) # Remove duplicates self.unlocked_entries = list(set(self.unlocked_entries)) self._sort_entries(entries=self.unlocked_entries) def _find_closest_free_number(self) -> int: """Find the closest unused EncEntry number.""" if len(self.all_entries) > 0: # Get all possible numbers last_number = cast(int, self.all_entries[-1].number) all_numbers = range(last_number + 1)[1:] used_numbers = [item.number for item in self.all_entries] free_numbers = set(all_numbers) - set(used_numbers) # If there are unused numbers. if len(free_numbers) > 0: return min(free_numbers) # Else add a new number. else: return len(self.all_entries) + 1 # Catch the first EncEntry to be entered. else: return 1
[docs] def add_entry(self, entry: ENTRY_TYPE) -> None: """Add an entry to the Encyclopaedia's internal lists and sorts it. Attempts to create duplicates are softly ignored. subjects list is updated when a new entry is added. Args: entry: The Entry to add to the Encyclopaedia """ if entry.parent is not None and entry.parent != self: raise AddEntryError( f"<{entry}> already has a parent: <{entry.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.all_entries if i.number == entry.number): raise AddEntryError(f"{entry.number} is already taken.") elif entry.number is None: entry.number = self._find_closest_free_number() self.all_entries.append(entry) entry.parent = self # Ensure no duplicates in the entry lists. self.all_entries = list(set(self.all_entries)) # Ensure correct sorting of entry lists. self._sort_entries(entries=self.all_entries) if entry.locked is False: self._add_entry_to_unlocked_entries(entry) self.subjects.append(entry.subject) self.subjects = list(set(self.subjects)) self.subjects.sort()
[docs] def sort(self, mode: Union[SortMode, None] = None, reverse: bool = False) -> None: """Sort the entries in the Encyclopaedia. The attribute `sorting_mode` will be set to the value of `mode`. Args: mode: The sorting mode to use. If None, the attribute `sorting_mode` will be used. reverse: If the sorting should be done in reverse or not. """ if mode: self._sorting_mode = mode self._reverse_sorting = reverse self._sort_entries(entries=self.current_entries)
@property def word_count(self) -> int: """Get the total word count for the Encyclopaedia. Return: The number of words in every EncEntry in the Encyclopaedia. """ count = 0 for entry in self.all_entries: count += entry.word_count return count def _build_subject_filter(self, subject: str) -> None: """Build an encyclopaedia's filtered_entries based on subject. Args: subject: The subject for the filter. """ if self.show_locked_buttons is False: entries = self.unlocked_entries else: entries = self.all_entries self.filtered_entries = [i for i in entries if i.subject == subject] def _change_active_entry_viewed_status(self) -> bool: """Change the viewed status of the active Entry. Return: True if status was changed, else False """ if self.active is None: raise ValueError( 'Tried to change active entry viewed status with no active entry.', ) if self.active.locked is False: entry = None if isinstance(self.active, Book): # When Book, set the first page to viewed, not the Book. entry = self.active.active else: entry = self.active # Mark the entry as viewed. entry.viewed = True return True return False
[docs] def set_entry(self, entry: ENTRY_TYPE) -> None: """Set an Entry as active. Args: entry: The Entry to set. Raises: ValueError: If the entry cannot be set. """ try: target_position = self.viewable_entries.index(entry) except ValueError as e: if entry not in self.all_entries: raise UnknownEntryError(f"{entry} is not in this Encyclopaedia") from e else: raise ValueError( f"{entry} cannot be set because it is locked and 'show_locked_entry' is False", ) from e self.current_position = target_position self.active = entry self._change_active_entry_viewed_status() # When sorting by Unread, setting an entry marks is as read. # Thus we have to resort the entries to ensure they appear in the # correct order. if self.sorting_mode == SortMode.UNREAD: self._sort_entries(entries=self.current_entries)
def _change_entry(self, direction: Direction) -> bool: """Change the active entry by changing the index. This is relative to the current sorting. Args: direction: The direction to move. """ test_position = self.current_position + direction.value # Boundary check if test_position < 0: return False if test_position >= len(self): return False # Update the current position. self.current_position += direction.value # Update the active entry. self.active = self.current_entry self._change_active_entry_viewed_status() # When changing an entry, the current entry page number is reset. self.active._unlocked_page_index = 0 return True
[docs] def previous_entry(self) -> bool: """Set the previous entry as the current entry.""" return self._change_entry(Direction.BACKWARD)
[docs] def next_entry(self) -> bool: """Set the next entry as the current entry.""" return self._change_entry(Direction.FORWARD)
[docs] def PreviousEntry(self) -> PreviousEntry: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return PreviousEntry(self)
[docs] def NextEntry(self) -> NextEntry: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return NextEntry(self)
[docs] def PreviousPage(self) -> PreviousPage: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return PreviousPage(encyclopaedia=self)
[docs] def NextPage(self) -> NextPage: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return NextPage(encyclopaedia=self)
[docs] def Sort(self, sorting_mode: SortMode, reverse: bool = False) -> SortEncyclopaedia: """Wrapper around the Action of the same name. Use with a renpy button. Args: sorting_mode: The type of sorting to use. If None specified, use the current sorting. reverse: Sort in reverse. Return: Screen Action """ return SortEncyclopaedia(self, sorting_mode, reverse)
[docs] def SetEntry(self, given_entry: 'EncEntry') -> SetEntry: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return SetEntry(self, given_entry)
[docs] def CloseActiveEntry(self) -> CloseActiveEntry: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return CloseActiveEntry(self)
[docs] def ResetSubPage(self) -> ResetSubPage: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return ResetSubPage(self)
[docs] def ToggleShowLockedButtons(self) -> ToggleShowLockedButtonsAction: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return ToggleShowLockedButtonsAction(self)
[docs] def ToggleShowLockedEntry(self) -> ToggleShowLockedEntryAction: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return ToggleShowLockedEntryAction(self)
[docs] def FilterBySubject(self, subject: str) -> FilterBySubject: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return FilterBySubject(self, subject)
[docs] def ClearFilter(self) -> ClearFilter: """Wrapper around the Action of the same name. Use with a renpy button. Return: Screen Action """ return ClearFilter(self)