Source code for encyclopaedia.encyclopaedia_ren

from typing import TYPE_CHECKING

from renpy import store

from .actions_ren import (
    ClearFilter,
    CloseActiveEntry,
    FilterBySubject,
    NextEntry,
    PreviousEntry,
    NextPage,
    PreviousPage,
    SortEncyclopaedia,
    SetEntry,
    ResetSubPage,
    ToggleShowLockedButtonsAction,
    ToggleShowLockedEntryAction,
)
from .entry_sorting_ren import push_locked_to_bottom
from .eventemitter_ren import EventEmitter
from .constants_ren import Direction, SortMode

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 cast, Callable, Optional, Union  # NOQA E402


[docs] class Encyclopaedia(EventEmitter, store.object): """Container that manages the behaviour of a collection of EncEntry objects. Args: sorting_mode: The default for how entries are sorted. Default sorting is by 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. 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: int = 0, show_locked_buttons: bool = False, show_locked_entry: bool = False, list_screen: str = 'encyclopaedia_list', entry_screen: str = 'encyclopaedia_entry', name: str = '', ) -> None: self.sorting_mode = SortMode(sorting_mode) 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.name = name self.all_entries: list['EncEntry'] = [] self.unlocked_entries: list['EncEntry'] = [] self.filtered_entries: list['EncEntry'] = [] self.filtering: Union[bool, str] = False self.reverse_sorting: bool = False if self.sorting_mode == SortMode.REVERSE_ALPHABETICAL: self.reverse_sorting = True self.nest_alphabetical_sort: bool = True self.current_position: int = 0 self.subjects: list[str] = [] self.active: Optional['EncEntry'] = None self._current_entries = self.all_entries self.locked_at_bottom: bool = True self.callbacks: dict[str, list[Callable[['Encyclopaedia'], 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.unlocked_entries) if self.show_locked_entry: rv = len(self.all_entries) return rv @property def current_entries(self) -> list['EncEntry']: """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) -> 'EncEntry': """Get the entry at current_position. If show locked entries, pull from all_entries. Else, pull from unlocked_entries. Return: EncEntry """ entry = self.unlocked_entries[self.current_position] if self.show_locked_entry: entry = self.all_entries[self.current_position] return entry @property def percentage_unlocked(self) -> float: """Get the percentage of the Encyclopaedia that's unlocked. Return: float: Percentage of the Encyclopaedia that's unlocked 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
[docs] def sort_entries( self, entries: list['EncEntry'], sorting: int = 0, reverse: bool = False, ) -> None: """Sort entry lists by whatever the current sorting mode is. Args: entries: The EncEntry list to sort sorting: The sorting mode to use reverse: If the sorting should be done in reverse or not """ sorting_mode = SortMode(sorting) if sorting_mode == SortMode.NUMBER: entries.sort(key=attrgetter('number')) else: entries.sort(reverse=reverse, key=attrgetter('name')) if sorting_mode == SortMode.UNREAD: entries.sort(key=attrgetter('viewed')) elif sorting_mode == SortMode.SUBJECT: entries.sort(key=attrgetter('subject')) if self.locked_at_bottom: push_locked_to_bottom(entries)
[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_entries.append(entry) # Remove duplicates self.unlocked_entries = list(set(self.unlocked_entries)) self.sort_entries( entries=self.unlocked_entries, sorting=int(self.sorting_mode.value), reverse=self.reverse_sorting, )
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: 'EncEntry') -> 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 ValueError( f"{entry} is already inside another Encyclopaedia", ) # 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 ValueError(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, sorting=int(self.sorting_mode.value), reverse=self.reverse_sorting, ) 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()
@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_entry(self, direction: Direction) -> bool: """Change the current active EncEntry.""" 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 if self.active.locked is False: # Run the callback, if provided. self.active.emit("viewed") # Mark the entry as viewed. self.active.viewed = True # 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) -> 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. Return: Screen Action """ return SortEncyclopaedia(self, sorting_mode)
[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)