code:
"""
01item.rpy

This module defines the TRNItem class, which represents items in the game with various properties such as name, cost, icon, image, status, and traits.

The same approach was taken here as in renpy/character.py: a factory pattern is used to allow for multiple item types to be hidden behind a single `Item` function.
"""

init offset = -100

python early:
    # Store the default item as None in the Ren'Py store.
    renpy.store.item = None

python early in trainer_item:

    # List to track different item categories and a dictionary to store unique items.
    categories = []
    uniques = {}

    class TRNItem(renpy.store.object):
        """
        Represents an item in the game.

        Args:
            name (str): The name of the item.
            cost (int, optional): The cost of the item. Default is None.
            icon (str, optional): The filename of the item's icon. Default is None.
            image (str, optional): The filename of the item's image. Default is None.
            icon_path (str, optional): The path to the item's icon directory. Default is 'images/items/icons'.
            image_path (str, optional): The path to the item's image directory. Default is 'images/items'.
            status (str, optional): The status of the item. Default is None.
            traits (list, optional): A list of traits associated with the item. Default is an empty list.

        Attributes:
            _name (str): The name of the item.
            _cost (int): The cost of the item.
            _icon (str): The path to the item's icon.
            _image (str): The path to the item's image.
            _status (str): The status of the item.
            _traits (list): A list of traits associated with the item.
        """

        def __new__(cls, name, kind=None, **properties):
            """
            Ensures that if the item is marked as unique, a new instance is only created
            if it doesn't already exist. Otherwise, reuses the existing instance.

            Args:
                name (str): The name of the item.
                kind (str, optional): The type of the item. Default is None.
                properties (dict): Additional properties for the item.

            Returns:
                TRNItem: A new or existing instance of the item.
            """
            if properties.get('unique', False):
                rv = uniques.get(name, None)

                if rv is None:
                    rv = super(TRNItem, cls).__new__(cls, name, kind, **properties)
                    uniques[name] = rv
                return uniques[name]
            else:
                return super(TRNItem, cls).__new__(cls, name, kind, **properties)

        def __init__(self, name, kind=None, **properties):
            """
            Initializes a new instance of the TRNItem class, setting various properties.

            Args:
                name (str): The name of the item.
                kind (str, optional): The type of the item. Default is None.
                properties (dict): Additional properties for the item.
            """
            if kind is None:
                kind = renpy.store.item

            # Helper function to extract property values from the item or the kind.
            def v(n):
                if n in properties:
                    return properties.pop(n)
                else:
                    return getattr(kind, "_{}".format(n))

            self._name = name
            self._category = v('category')
            self._cost = v('cost')
            self._dynamic = v('dynamic')
            self._effect = v('effect')
            self._status = v('status')
            self._traits = v('traits')
            self._icon = v("icon")
            self._image = v("image")
            self._to_use = v('to_use')
            self._to_see = v('to_see')
            self._to_give = v('to_give')

            # Add the category to the global list if it's not already present.
            if self._category and self._category not in categories:
                categories.append(self._category)

            # Build full paths for the icon and image.
            if self._icon:
                self._icon = '/'.join([icon_path, self._icon])
            if self._image:
                self._image = '/'.join([image_path, self._image])

        def __str__(self):
            """
            Returns the name of the item, evaluating dynamic names if necessary.

            Returns:
                str: The name of the item.
            """
            what = self._name

            if self._dynamic:
                what = renpy.python.py_eval(self._name)

            rv = renpy.substitutions.substitute(what)[0]

            if PY2:
                rv = rv.encode("utf-8")

            return rv

        def __format__(self, spec):
            """
            Formats the item as a string according to the given format specification.

            Args:
                spec (str): The format specification.

            Returns:
                str: The formatted string.
            """
            return format(str(self), spec)

        def __repr__(self):
            """
            Provides a string representation of the item for debugging purposes.

            Returns:
                str: A string representation of the item.
            """
            return "<Item: {!r}>".format(self.__str__())

        #region Properties

        @property
        def category(self):
            """
            Returns the category of the item.

            Returns:
                str: The category of the item.
            """
            return self._category

        @property
        def cost(self):
            """
            Returns the cost of the item.

            Returns:
                int: The cost of the item. Defaults to 0 if not set.
            """
            return self._cost if self._cost is not None else 0

        @property
        def effect(self):
            """
            Returns the effect of the item.

            Returns:
                str: The effect of the item.
            """
            return self._effect

        @property
        def icon(self):
            """
            Returns the path to the item's icon.

            Returns:
                str: The path to the item's icon.
            """
            return self._icon

        @property
        def image(self):
            """
            Returns the path to the item's image.

            Returns:
                str: The path to the item's image.
            """
            return self._image

        @property
        def name(self):
            """
            Returns the name of the item.

            Returns:
                str: The name of the item.
            """
            return self.__str__()

        @property
        def status(self):
            """
            Returns the status of the item.

            Returns:
                str: The status of the item.
            """
            return self._status

        @status.setter
        def status(self, status):
            """
            Sets the status of the item.

            Args:
                status (str): The new status of the item.
            """
            self._status = status

        @property
        def traits(self):
            """
            Returns the list of traits associated with the item.

            Returns:
                list: The list of traits.
            """
            return self._traits

        #endregion

        #region Methods

        def add_trait(self, trait):
            """
            Adds a trait to the item.

            Args:
                trait (str): The trait to add.
            """
            self._traits.append(trait)

        def check_condition(self, condition):
            """
            Checks a condition related to the item (e.g., 'give', 'see', 'use').

            Args:
                condition (str): The condition to check.

            Raises:
                ValueError: If the condition is invalid.
            """
            try:
                if condition.lower() == 'give':
                    pass
                elif condition.lower() == 'see':
                    pass
                elif condition.lower() == 'use':
                    pass
                else:
                    raise ValueError("Invalid condition")
            except ValueError as error:
                raise error

        def remove_trait(self, trait):
            """
            Removes a trait from the item.

            Args:
                trait (str): The trait to remove.

            Raises:
                ValueError: If the trait is not found in the item's traits.
            """
            try:
                if trait in self._traits:
                    self._traits.remove(trait)
                else:
                    raise ValueError(f"Trait '{trait}' not found.")
            except ValueError as error:
                raise error

        def use_item(self):
            """
            Placeholder for the method to use an item. Should be implemented later.
            """
            pass

        #endregion

    # Assign TRNItem to the Ren'Py store.
    renpy.store.TRNItem = TRNItem

    def Item(name, kind=None, **properties):
        """
        Factory function to create new items using TRNItem.

        Args:
            name (str): The name of the item.
            kind (str, optional): The type of the item. Defaults to renpy.store.item.

        Returns:
            TRNItem: A new instance of the TRNItem class.
        """
        if kind is None:
            kind = renpy.store.item

        kind = getattr(kind, "trainer_item", kind)

        return type(kind)(name, kind=kind, **properties)

    # Assign Item function to the Ren'Py store.
    renpy.store.Item = Item

init -800 python:
    # Define a default TRNItem to serve as the base for all other items.
    renpy.store.item = TRNItem(None,
        unique=False,
        category=None,
        cost=None,
        dynamic=False,
        effect=None,
        icon=None,
        image=None,
        icon_path='images/items/icons',
        image_path='images/items',
        status=None,
        traits=[],
        to_use=[],
        to_see=[],
        to_give=[],
        kind=False
    )


code:
"""
01locations.rpy

Location Class allows for quick creation of new locations.
"""

python early in location:
    import re
    from store.dynamicentry import DynamicEntry
    from store.layeredimage import LayeredImage
    from store.trainer_item import Inventory
    from store import trainer

    locations = []
    trainer.background_segments = []
    trainer.stay_message = "Don't go anywhere"

    def image_function(trimmed_name, path, segments = None, variants = None):
        """
        Registers background images for different times of the day.

        Args:
            trimmed_name (str): The trimmed name of the location.
            path (str): The path to the images.
        """
        if not segments and not variants:
            renpy.image("bg {}".format(trimmed_name), "_".join([path, trimmed_name]))
        else:
            for segment in segments:
                if variants:
                    for variant in variants:
                        renpy.image("bg {}".format(trimmed_name, "_".join([segment, variant])))
                else:
                    renpy.image("bg {}".format(trimmed_name, "_".join([segment])))

    class Location(DynamicEntry):
        """
        Represents a story or logical location in the game, with attributes for characters, items, accessibility, and events.

        Args:
            name (str): The name of the location.
            adjacent (list): A list of names of adjacent locations.
            **properties: Additional properties for the location.

        Attributes:
            name (str): The name of the location in snake_case format.
            accessible (bool): Indicates if the location is accessible.
            adjacents (list): List of adjacent locations.
            background (str): The name of the background image based on the time of day or other criteria.
            inventory (list): The inventory of items at the location.
            characters (list): List of characters present at the location.
            is_locked (bool): Indicates if the location is locked.
            is_hidden (bool): Indicates if the location is hidden.
            is_inaccessible (bool): Indicates if the location is inaccessible.
            outfit_type (str): The outfit type appropriate for the location (swimwear, school uniform, etc.).
            events (list): List of events at the location.
            is_private (bool): Indicates if the location is a private space.
            is_public (bool): Indicates if the location is a public space.
            is_special (bool): Indicates if the location is a special space like a pool or locker room.
        """

        def __init__(self, name, adjacents=None, image_function=image_function, **properties):

            # Adjacent locations
            self._adjacents = []

            # General properties
            self._accessible = properties.pop('accessible', True)
            self._locked = properties.pop('locked', False)
            self._hidden = properties.pop('hidden', False)
            self._inaccessible = properties.pop('inaccessible', False)
            self._inventory = Inventory(properties.pop('inventory', []))
            self._characters = []
            self._outfit_type = properties.pop('outfit_type', None)
            self._events = properties.pop('events', [])
            self._private = properties.pop('private', False)
            self._public = properties.pop('public', True)
            self._special = properties.pop('special', False)

            # Background handling
            self._background = properties.pop('background', None)
            self._image_function = image_function
            self._day_cycle = properties.pop('day_cycle', False)
            self._path = properties.pop('path', "/images/backgrounds/")
            if isinstance(self._background, LayeredImage):
                self._day_cycle = True
                self._variants = self._background.attributes
            if self._background is None:
                self._variants = properties.pop('variants', [])
                self._image_function(self.__trim(name), self._path, properties.pop('segments', trainer.background_segments), self._variants)

            super().__init__(name=name, kind='location', **properties)

            # Create adjacency relationships
            self._menu_list.append((trainer.stay_message, (self.trimmed, "location")))
            for adjacent in adjacents:
                self.add_adjacent(adjacent)
            locations.append(self)

        def __str__(self):
            what = "{} | {}".format(self._name, self.trimmed)

            if PY2:
                what = what.encode("utf-8")

            return what

        def __repr__(self):
            """
            Provides a string representation of the item for debugging purposes.

            Returns:
                str: A string representation of the item.
            """
            return "<Location: {!r}>".format(self.__str__())

        @property
        def accessible(self):
            """Gets the accessibility status of the location."""
            return self._accessible

        @accessible.setter
        def accessible(self, value):
            """Sets the accessibility status of the location."""
            self._accessible = value

        @property
        def adjacents(self):
            """Gets the adjacent locations."""
            return self._adjacents

        @property
        def background(self):
            """Gets the background image based on the time of day or criteria."""
            if self._day_cycle:
                return f"bg {self._trimmed}_{current_time.lower()}"
            return self._background

        @property
        def characters(self):
            """Gets the characters at the location."""
            return self._characters

        @property
        def events(self):
            """Gets the events at the location."""
            return self._events

        @property
        def inventory(self):
            """Gets the inventory of items at the location."""
            return self._inventory

        @property
        def is_locked(self):
            """Checks if the location is locked."""
            return self._locked

        @is_locked.setter
        def is_locked(self, locked):
            """Sets the locked status of the location."""
            self._locked = locked

        @property
        def is_hidden(self):
            """Checks if the location is hidden."""
            return self._hidden

        @is_hidden.setter
        def is_hidden(self, hidden):
            """Sets the hidden status of the location."""
            self._hidden = hidden

        @property
        def is_inaccessible(self):
            """Checks if the location is inaccessible."""
            return self._inaccessible

        @is_inaccessible.setter
        def is_inaccessible(self, inaccessible):
            """Sets the inaccessible status of the location."""
            self._inaccessible = inaccessible

        @property
        def is_private(self):
            """Checks if the location is a private space."""
            return self._private

        @property
        def is_public(self):
            """Checks if the location is a public space."""
            return self._public

        @property
        def is_special(self):
            """Checks if the location is a special location like a pool or locker room."""
            return self._special

        @property
        def name(self):
            """Gets the name of the location."""
            return self._name

        @property
        def outfit_type(self):
            """Gets the outfit type associated with the location."""
            return self._outfit_type

        @outfit_type.setter
        def outfit_type(self, outfit_type):
            """Sets the outfit type for the location."""
            self._outfit_type = outfit_type

        @property
        def trimmed(self):
            """Gets the name of the location in snake_case format."""
            return self.__trim(self.name)

        def add_adjacent(self, adjacent):
            """Adds an adjacent location."""
            if isinstance(adjacent, Location):
                if adjacent not in self._adjacents:
                    self._adjacents.append(adjacent)
                    self._menu_list.insert(-2, (adjacent.name, (adjacent.trimmed, "location")))
                    adjacent.add_adjacent(self)
            if isinstance(adjacent, str):
                for location in locations:
                    if location.name == adjacent or location.trimmed == adjacent:
                        self.add_adjacent(location)
                        break
            else:
                raise TypeError("Adjacent is not a Location.")

        def add_item(self, item):
            """Adds an item to the location."""
            self._inventory.append(item)

        def add_person(self, person):
            """Adds a character to the location."""
            self._characters.append(person)

        def clear_location(self):
            """Clears all characters from the location."""
            self._characters.clear()

        def has_item(self, item):
            """Checks if an item is present at the location."""
            return item in self._inventory

        def has_person(self, person):
            """Checks if a character is present at the location."""
            return person in self._characters

        def remove_item(self, item):
            """Removes an item from the location."""
            self._inventory.remove(item)

        def remove_person(self, person):
            """Removes a character from the location."""
            self._characters.remove(person)

        def __trim(self, string):
            return re.sub('[ ]','_',re.sub("[']",'',string)).lower()

    class LocationHandler(renpy.store.object):
        """
        Currently this class is used to find all location objects and add/remove them (for the Scheduler see 01scheduler.rpy).
        Review: Might be temporary, but could be more helpful later
        TODO: Document
        """
        def __new__(cls):
            if not hasattr(cls, 'instance'):
                cls.instance = super(LocationHandler, cls).__new__(cls)
                return cls.instance

        def __init__(self):
            self._locations = {}

        def add_location(self, location):
            """Adds a new location object"""
            self._locations[location.name] = location

        def remove_location(self, location):
            """Removes a location object from the main list"""
            self._locations[location.name] = None

        def find_location(self, location_name):
            """Gives a location object from the appropriate name"""
            name = re.sub('[ ]', '_', re.sub("[']", '', location_name)).lower()
            if name in self._locations:
                return self._locations[name]
            return f"Unable to get the specified location, please make sure {location_name} is spelled properly"

    def parse_location(l):
        """
        Parses a location block.
        """

        # Parse the name of the location
        name = l.require(l.image_name_component)

        # Create a dictionary to hold location properties
        properties = {}
        # Create a list to hold the adjacent locations
        adjacent = []

        # Parse properties until the end of the block
        while not l.eol():
            line = l.rest().strip()

            if line == "public":
                properties['public'] = True
            elif line == "private":
                properties['public'] = False
            elif line == "cycle":
                properties['day_cycle'] = True
            elif line == "not accessible":
                properties['accessible'] = False
            elif line.startswith('"'):
                properties['path'] = line.strip('"')
            elif line.startswith("connects:"):
                # Parse the adjacent locations
                adjacent = parse_adjacent(l)
            else:
                l.error(f"Unknown statement: {line}")

            # Move to the next line
            l.advance()

        # Create the Location object with the parsed properties
        return Location(name, adjacent, **properties)

    def parse_adjacent(l):
        """
        Parses adjacent locations.
        """
        adjacent = []

        # Advance to the next line
        l.advance()

        # Parse adjacent locations until the end of the block
        while not l.eol():
            line = l.rest().strip()

            if line:
                parts = line.split('"')
                name = parts[0].strip()
                caption = parts[1] if len(parts) > 1 else None
                adjacent.append((name, caption))

            # Move to the next line
            l.advance()

        return adjacent

    # Register the statement
    renpy.register_statement("location", parse=parse_location, execute=None, init=False, block=True)

    renpy.store.Location = Location
    renpy.store.location_handler = LocationHandler()

repo_name: pervy/rogues

license: unknown


code:
"""
    01paperdoll.rpy

    Clothing
"""
init offset = -99

python early in paperdoll:
    import re
    from store import Transform, ConditionSwitch, Fixed, Null, config, Text, eval, At
    from collections import OrderedDict, defaultdict
    from store.layeredimage import Layer, LayeredImage, execute_layeredimage

    #region Exceptions

    # Exceptions for logging the Outfit system
    class TRNCharacterMissingException(Exception):
        """Exception raised when a TRN character is missing."""
        pass

    class ClothingMissingException(Exception):
        """Exception raised when clothing is missing in the outfit system."""
        pass

    class PoseMissingException(Exception):
        """Exception raised when a pose is missing in the paperdoll system."""
        pass

    #endregion Exceptions

    #region Layers

    #TODO: BodyAttribute should be able to link to character states.
    class BodyAttribute(Layer):
        """
        Represents a body attribute for the paper doll.

        Body attributes are components of the paper doll representing different body parts or features.

        Attributes:
            group (str): The group the attribute is part of.
            attribute (str): The name of the attribute.
            image: The image associated with the attribute.
            default (bool): If True, this attribute is selected by default.
            group_args (dict): Additional arguments for the group.
            prefix (str): The prefix for the attribute.
            variant (str): The variant for the attribute.

        Methods:
            __init__: Initializes a BodyAttribute object.
            apply_format: Applies formatting to the image.
            get_displayable: Gets the displayable image.
        """

        def __init__(self, group, attribute, image=None, default=False, group_args=None, **properties):

            self.attribute = attribute
            self.group = group
            self.image = image
            self.default = default
            self.prefix = properties.pop("prefix", None)
            self.variant = properties.pop("variant", None)

            self.raw_attribute = attribute

            if self.prefix is not None:
                self.attribute = f"{self.prefix}_{self.attribute}"

            super().__init__(group_args=group_args, **properties)

        def apply_format(self, ai):
            """
            Applies formatting to the image.

            Args:
                ai: Reference to the layeredimage object calling this method.
            """
            self.image = self.wrap(ai.format(
                "Body ({!r})".format(self.attribute),
                variant=self.variant,
                attribute=self.raw_attribute,
                image=self.image,
                ))

        def get_displayable(self, attributes):
            """
            Gets the displayable image.

            Args:
                attributes (list): The list of attributes.

            Returns:
                str or None: The displayable image.
            """
            if self.attribute not in attributes:
                return None

            if not self.check(attributes):
                return None

            return self.image

    #TODO: Should take in reference to character property to link state to.
    class RawBodyAttribute(object):
        """Represents a raw body attribute for the paper doll.

        Raw body attributes are used to create BodyAttribute objects.

        Attributes:
            name (str): The name of the attribute.
            image (str): The image associated with the attribute.
            properties (OrderedDict): Additional properties for the attribute.

        Methods:
            __init__: Initializes a RawBodyAttribute object.
            execute: Executes the creation of a BodyAttribute object.
        """

        def __init__(self, name):

            self.name = name
            self.image = None
            self.properties = OrderedDict()

        def execute(self, group=None, group_properties=None):
            """Executes the creation of a BodyAttribute object.

            Args:
                group (str, optional): The group the attribute belongs to.
                group_properties (dict, optional): Additional properties for the group.

            Returns:
                list: A list containing the created BodyAttribute object.
            """
            if group_properties is None:
                group_properties = {}

            if self.image:
                image = eval(self.image)
            else:
                image = None

            properties = {k: v for k, v in group_properties.items() if k not in ATL_PROPERTIES_SET}
            group_args = {k: v for k, v in group_properties.items() if k in ATL_PROPERTIES_SET}
            properties.update({k: eval(v) for k, v in self.properties.items()})

            return [BodyAttribute(group, self.name, image, group_args=group_args, **properties)]

    class BodyGroup(Layer):
        """
        Represents a group of body attributes for the paper doll.

        Body groups are composed of BodyAttributeobjects, representing different body parts
        and facial expressions, respectively.

        Attributes:
            attributes (list): List of BodyAttribute objects in this group.
            group (str): The name of the group this BodyGroup belongs to.
            wardrobe: The Wardrobe object responsible for managing the current outfit.

        Methods:
            __init__: Initializes a BodyGroup object.
            apply_format: Applies formatting to the composed image.
            get_displayable: Gets the displayable image of the composed BodyGroup.
        """

        def __init__(self, group, attributes, wardrobe, **properties):

            self.attributes = attributes
            self.group = group
            self.wardrobe = wardrobe

            self._multiple = properties.pop("multiple", False)

            super().__init__(group_args={}, **properties)

        def apply_format(self, ai):
            """
            Applies formatting to the composed image.

            Args:
                ai: Reference to the layeredimage object calling this method.
            """
            for i in self.attributes:
                i.apply_format(ai)

        def get_displayable(self, attributes):
            """
            Gets the displayable image of the composed BodyGroup.

            Args:
                attributes (list): List of active attributes.

            Returns:
                Displayable: The composed image of all active attributes.
            """
            attributes.extend(self.wardrobe.get_current_group(self.group))
            if (len(self.attributes) == 1) and not self.attributes[0].check(attributes):
                if self.attributes[0]._duplicatable:
                    return self.attributes[0]._duplicate(None)
                else:
                    return self.attributes[0].get_displayable(attributes)
            else:
                rv = Fixed(**transform_args)

                for attr in self.attributes:
                    if not attr.check(attributes):
                        continue
                    d = attr.get_displayable(attributes)
                    if d is not None:
                        if d._duplicatable:
                            d = d._duplicate(None)
                        rv.add(d)
                return rv

    class RawBodyGroup(object):
        """
        Represents a raw body group for the paper doll.

        Raw body groups are used to define body groups and their attributes.

        Attributes:
            group (str): The name of the body group.
            properties (OrderedDict): Additional properties for the body group.

        Methods:
            __init__: Initializes a RawBodyGroup object.
            execute: Executes the raw body group, creating body attributes and groups.
        """

        def __init__(self, group, character):
            self.group = group
            self.properties = OrderedDict()
            self.children = [ ]
            self.wardrobe = get_wardrobe(character)

        def execute(self):
            """
            Executes the raw body group, creating body attributes and groups.

            Returns:
                list: A list of body groups created.
            """
            properties = { k : eval(v) for k, v in self.properties.items() }

            auto = properties.pop("auto", False)
            variant = properties.get("variant", None)
            multiple = properties.pop("multiple", False)

            rv = [ ]

            for i in self.children:
                rv.extend(i.execute(group=self.group, group_properties=properties))

            if auto:
                seen = set(i.raw_attribute for i in rv)
                pattern = self.image_name.replace(" ", "_")  + "_" + self.group + "_"

                if variant:
                    pattern += variant + "_"

                for i in renpy.list_images():

                    if i.startswith(pattern):
                        rest = i[len(pattern):]
                        attrs = rest.split()

                        if len(attrs) == 1:
                            if attrs[0] not in seen:
                                rv.append(BodyAttribute(self.group, attrs[0], renpy.displayable(i), **properties))

    class ClothingAttribute(Layer):
        """
        Represents an attribute for clothing on a paper doll.

        Clothing attributes are components of the paper doll representing different clothing items.

        Attributes:
            attribute (str): The name of the clothing attribute.
            image (str): The image associated with the clothing attribute.
            default (bool): If True, this attribute is selected by default.
            variant (str): The variant for the clothing attribute.
            group_args (dict): Additional arguments for the group.

        Methods:
            __init__: Initializes a ClothingAttribute object.
            apply_format: Applies formatting to the image.
            get_displayable: Gets the displayable image.
        """

        def __init__(self, attribute, image=None, default=False, group_args=None, **properties):

            prefix = properties.pop("prefix", None)
            variant = properties.pop("variant", None)

            if prefix is not None:
                attribute = f"{prefix}_{attribute}"

            self.attribute = attribute
            self.image = image
            self.default = default
            self.variant = variant
            self.group_args = group_args

            super().__init__(group_args=self.group_args, **properties)

        def apply_format(self, ai):
            """
            Applies formatting to the image.

            Args:
                ai: Reference to the layeredimage object calling this method.
            """
            self.image = self.wrap(ai.format(
                "Clothing ({!r})".format(self.attribute),
                attribute=None,
                image=self.image,
                ))

        def get_displayable(self, attributes):
            """
            Gets the displayable image.

            Args:
                attributes (list): The list of attributes.

            Returns:
                str or None: The displayable image.
            """
            if self.attribute not in attributes:
                return None

            if not self.check(attributes):
                return None

            return self.image

    class ClothingGroup(Layer):
        """
        Represents a group of clothing attributes in a paper doll.

        Clothing groups are used to combine multiple clothing attributes into a single image for the paper doll.

        Attributes:
            group (str): The name of the clothing group.
            clothes (list): A list of clothing attributes in the group.
            wardrobe (Wardrobe): The wardrobe object controlling the clothing.

        Methods:
            __init__: Initializes a ClothingGroup object.
            apply_format: Applies formatting to the images.
            get_displayable: Gets the displayable image.
        """

        def __init__(self, group, clothes, wardrobe, **properties):

            self.group = group
            self.clothes = clothes
            self.wardrobe = wardrobe

            self._multiple = properties.pop("multiple", False)

            super().__init__(group_args=None, **properties)

        def apply_format(self, ai):
            """
            Applies formatting to the images.

            Args:
                ai: Reference to the layeredimage object calling this method.
            """
            for i in self.clothes:
                i.apply_format(ai)

        def get_displayable(self, attributes):
            """
            Gets the displayable image.

            Args:
                attributes (list): The list of attributes.

            Returns:
                Fixed: The composed image of all clothing attributes in the group.
            """
            attributes.extend(self.wardrobe.get_current_group(self.group))

            if (len(self.clothes) == 1) and not self.clothes[0].check(attributes):
                if self.clothes[0]._duplicatable:
                    return self.clothes[0]._duplicate(None)
                else:
                    return self.clothes[0].get_displayable(attributes)
            else:
                rv = Fixed(**transform_args)

                for c in self.clothes:
                    if not c.check(attributes):
                        continue

                    d = c.get_displayable(attributes)

                    if d is not None:
                        if d._duplicatable:
                            d = d._duplicate(None)

                        rv.add(d)

                return rv

    class RawClothingGroup(object):
        """
        Represents a raw clothing group for the paper doll.

        Raw clothing groups are used to define clothing groups and their attributes.

        Attributes:
            group (str): The name of the clothing group.
            parent (Layer): The parent layer containing the raw clothing group.
            properties (OrderedDict): Additional properties for the clothing group.

        Methods:
            __init__: Initializes a RawClothingGroup object.
            execute: Executes the raw clothing group, creating clothing attributes and groups.
        """

        def __init__(self, group, parent):

            self.group = group
            self.parent = parent
            self.properties = OrderedDict()

            names = parent.name.split()
            self.wardrobe = get_wardrobe(names[0])

        def execute(self):
            """
            Executes the raw clothing group, creating clothing attributes and groups.

            Returns:
                list: A list of clothing groups created.
            """
            properties = { k : eval(v) for k, v in self.properties.items() }

            auto = properties.pop("auto", False)
            variant = properties.get("variant", None)
            multiple = properties.pop("multiple", False)

            c = [ ]

            if multiple:
                group = None
            else:
                group = self.group

            if auto:
                pattern = self.group + "_"

                if variant:
                    pattern += variant + "_"

                for i in renpy.list_images():

                    if i.startswith(pattern):
                        rest = i[len(pattern):]
                        attrs = rest.split()

                        if len(attrs) == 1:
                            c.append(ClothingAttribute(group, attrs[0], renpy.displayable(i), **properties))

            for i in self.parent.children:
                if isinstance(i, ClothingGroup):
                    if i.group == self.group:
                        i.children.extend(c)
                        return []

            return [ ClothingGroup(self.group, c, self.wardrobe) ]

    #TODO: Expresion should have range(s) specified
    class ExpressionAttribute(Layer):
        """
        Represents an expression attribute for the paper doll.

        Expression attributes are components of the paper doll representing different facial expressions.

        Attributes:
            group (str): The group the attribute is part of.
            attribute (str): The name of the attribute.
            image: The image associated with the attribute.
            default (bool): If True, this attribute is selected by default.
            group_args (dict): Additional arguments for the group.

        Methods:
            __init__: Initializes an ExpressionAttribute object.
            apply_format: Applies formatting to the images.
            get_displayable: Gets the displayable images.
        """

        def __init__(self, attribute, image, default=False, group_args=None, **properties):

            self.attribute = attribute
            self.images = image
            self.default = default
            self.group_args = group_args
            self.prefix = properties.pop("prefix", None)
            self.variant = properties.pop("variant", None)

            self.raw_attribute = attribute

            if self.prefix is not None:
                self.attribute = f"{self.prefix}_{self.attribute}"

            super().__init__(group_args=self.group_args, **properties)

        def apply_format(self, ai):
            """
            Applies formatting to the images.

            Args:
                ai: Reference to the layeredimage object calling this method.
            """

            self.image = self.wrap(ai.format(
                "Expression ({!r})".format(self.attribute),
                variant=self.variant,
                attribute=self.raw_attribute,
                image=self.image,
                ))

        def get_displayable(self, attributes):
            """
            Gets the displayable images.

            Args:
                attributes (list): The list of attributes.

            Returns:
                list or None: The displayable images.
            """
            if self.attribute not in attributes:
                return None

            if not self.check(attributes):
                return None

            return self.images

    class RawExpressionAttribute(object):
        """Represents a raw expression attribute for the paper doll.

        Raw expression attributes are used to create ExpressionAttribute objects.

        Attributes:
            name (str): The name of the attribute.
            images (list): The list of images associated with the attribute.
            properties (OrderedDict): Additional properties for the attribute.

        Methods:
            __init__: Initializes a RawExpressionAttribute object.
            execute: Executes the creation of an ExpressionAttribute object.
        """

        def __init__(self, name):

            self.name = name
            self.images = []
            self.properties = OrderedDict()

        def execute(self, group=None, group_properties=None):
            """Executes the creation of an ExpressionAttribute object.

            Args:
                group (str): The group the attribute belongs to.
                group_properties (dict): Additional properties for the group.

            Returns:
                list: A list containing the created ExpressionAttribute object.
            """
            if group_properties is None:
                group_properties = {}

            if self.images:
                images = [eval(image) for image in self.images]
            else:
                images = None

            properties = {k: v for k, v in group_properties.items() if k not in ATL_PROPERTIES_SET}
            group_args = {k: v for k, v in group_properties.items() if k in ATL_PROPERTIES_SET}
            properties.update({k: eval(v) for k, v in self.properties.items()})

            return [ExpressionAttribute(self.name, images, group=group, group_args=group_args, **properties)]

    #endregion Layers

    #region CDS

    def parse_body_group(l, parent):
        """
        Parses a BodyGroup.
        """
        group = l.require(l.image_name_component)

        rv = RawBodyGroup(group, parent)
        parent.children.append(rv)

        while parse_property(l, rv, ["auto", "prefix", "variant", "multiple"] + LAYER_PROPERTIES):
            pass

        if l.match(':'):

            l.expect_eol()
            l.expect_block("body")

            ll = l.subblock_lexer()

            while ll.advance():
                while parse_property(ll, rv, ["auto"] + LAYER_PROPERTIES):
                    pass

                ll.expect_eol()
                ll.expect_noblock('body property')

        else:
            l.expect_eol()
            l.expect_noblock("body")

    def parse_clothing_group(l, parent):
        """
        Parses a ClothingGroup.
        """
        group = l.require(l.image_name_component)

        rv = RawClothingGroup(group, parent)
        parent.children.append(rv)

        while parse_property(l, rv, [ "auto", "prefix", "variant", "multiple" ] + LAYER_PROPERTIES):
            pass

        if l.match(':'):

            l.expect_eol()
            l.expect_block("clothing")

            ll = l.subblock_lexer()

            while ll.advance():
                while parse_property(ll, rv, [ "auto" ] + LAYER_PROPERTIES):
                    pass

                ll.expect_eol()
                ll.expect_noblock('clothing property')

        else:
            l.expect_eol()
            l.expect_noblock("clothing")

    def parse_paperdoll(l):
        """
        Parses a paperdoll block. Copied parse_layeredimage() from 00layeredimage.rpy
        """

        name = [ l.require(l.image_name_component) ]

        while True:
            part = l.image_name_component()

            if part is None:
                break

            name.append(part)

        l.require(':')
        l.expect_block("paperdoll")

        ll = l.subblock_lexer()
        ll.advance()

        name = " ".join(name)
        rv = RawLayeredImage(name)

        while not ll.eob:

            if ll.keyword('attribute'):
                # Parse an attribute
                parse_attribute(ll, rv)
                ll.advance()

            elif ll.keyword('group'):
                # Parse a general group
                parse_group(ll, rv, name)
                ll.advance()

            elif ll.keyword('clothing'):
                # Parse a clothing group (specific to paperdolls)
                parse_clothing_group(ll, rv)
                ll.advance()

            elif ll.keyword('body_group'):
                # Parse a body group (specific to paperdolls)
                parse_body_group(ll, rv)
                ll.advance()

            elif ll.keyword('emotion_group'):
                # Parse an emotion group (specific to paperdolls)
                parse_emotion_group(ll, rv)
                ll.advance()

            elif ll.keyword('if'):
                # Parse conditions
                parse_conditions(ll, rv)
                # Advances for us

            elif ll.keyword('always'):
                # Parse always block
                parse_always(ll, rv)
                ll.advance()

            else:

                while parse_property(ll, rv, [ "image_format", "format_function", "attribute_function", "offer_screen", "at" ] +
                    renpy.sl2.slproperties.position_property_names +
                    renpy.sl2.slproperties.box_property_names +
                    ATL_PROPERTIES
                    ):
                    pass

                ll.expect_noblock('statement')
                ll.expect_eol()
                ll.advance()

        return rv

    renpy.register_statement("paperdoll", parse=parse_paperdoll, execute=execute_layeredimage, init=True, block=True)

    #endregion ClothingCDS

    renpy.store.ClothingAttribute = ClothingAttribute


code:
init python early:
    import re

    # vars accepts tuples for variable type and value
    def get_parser_variables(lexer, param_dict, vars):
        for var in vars:
            if (var[1] == "int"):
                para_dict[var[0]] = lexer.int()
            elif (var[1] == "string"):
                para_dict[var[0]] = lexer.name()
            elif (var[1] == "bool"):
                para_dict[var[0]] = lexer.name()
            elif (var[1] == "string_list"):
                para_dict[var[0]] = lexer.rest()

        return param_dict

    def set_bool_param(lexer, param_dict, var_name):
        if lexer.keyword(var_name):
            param_dict[var_name] = True

    def set_string_param(lexer, param_dict, var_name):

        if lexer.keyword(var_name):
            param_dict[var_name] = lexer.rest()

    def set_int_param(lexer, param_dict, var_name):

        if lexer.keyword(var_name):
            param_dict[var_name] = lexer.integer()


code:
""" 01phone.rpy
"""

init -900 python in trainer:
    from collections import namedtuple

    Phone_Button = namedtuple('Phone_Button', ['name', 'image'])


code:
#TODO: Clean up this file.

init -1 python in trainer:

    import renpy.store as store

    def change_relationship(person_1, person_2, relationship):
        relationship_info = store.relationship_types[relationship]
        if getattr(person_1, relationship_info[0]) >= relationship_info[1]:
            person_1.relationships[give_index(person_1.relationships, person_2)][1]["relationship"] = relationship
            person_2.relationships[give_index(person_2.relationships, person_1)][1]["relationship"] = relationship
            return True
        else:
            return False

    def change_own_petname(person_1, person_2, petname):
        petname_info = store.self_petnames[petname]
        if petname_check(person_1, petname):
            person_2.relationships[give_index(person_2.relationships, person_1)][1]["petname"] = petname
            return True
        else:
            return False

    def change_others_petname(person_1, person_2, petname):
        petname_info = store.others_petnames[petname]
        if petname_check(person_1, petname):
            person_1.relationships[give_index(person_1.relationships, person_2)][1]["petname"] = petname
            return True
        else:
            return False

    def reset_own_petname(person_1, person_2):
        if person_2.relationships[give_index(person_2.relationships, person_1)][1]["petname"] == person_1._real_name:
            return False
        else:
            person_2.relationships[give_index(person_2.relationships, person_1)][1]["petname"] = person_1._real_name
            return True

    def reset_others_petname(person_1, person_2):
        if person_1.relationships[give_index(person_1.relationships, person_2)][1]["petname"] == person_2._real_name:
            return False
        else:
            person_1.relationships[give_index(person_1.relationships, person_2)][1]["petname"] = person_2._real_name
            return True


code:
"""
    01scheduler.rpy

"""

init -900 python in trainer:

    class Scheduler(renpy.store.object):
        """
        TODO: Document
        """
        def __init__(self, schedulee, schedule = None):
            """
            Initializes a schedule for a certain object

            :param schedule: Should contain a list of action(s) for a certain time interval
            :type schedule: dictionary
            """
            self._schedule = schedule # Going to assume this a dictionary class -- probably made from CDS
            self._schedulee = schedulee

            for k, v in self._schedule.items():
                if (isinstance(v, renpy.store.Location)):
                    print("Is a location")

                if (isinstance(v, str)):
                    scheduled_location = renpy.store.location_handler.find_location(v)
                    if scheduled_location is not None:
                        self._schedule[k] = scheduled_location
                    else:
                        self._self._schedule[k] = None

                # if (isinstance(v, 'action')):
                #     print('is a action')

        # def on_time_increment(self):
        #     """
        #     Does scheduled action if valid
        #     """

        #     time_interval = trainer.time.time

        def update(self):
            """
            Updates the schedulee's location based on the current time SEE @01time.rpy
            """

            location = self._schedule[renpy.store.time.interval] # FIXME: Ugly as f^%$ but works -- don't do it too often and think of better way to pull global time

            self._schedulee.move(location)


code:
"""
    01screens.rpy
    This file has the pre-built screen elements to interact with renpy-trainer.
"""

screen get_clothes(who, kind):
    style_prefix trainer.gui_prefix + "wardrobe"

    # viewport mousewheel "change":
        # for clothing in who.clothes[kind]:
            # imagebutton [clothing.image] action Function(who.select_clothing(kind, clothing))

screen clothing_list(who):
    style_prefix trainer.gui_prefix + "wardrobe"

    # viewport "horizontal-change":
    #     for key,value in who.clothing:
    #         use screen get_clothes(who, key)

define image_size = (203, 190*2)

# style vertical_menu_frame:
#     background Solid("#000000")

screen vertical_menu(_type = "image", commands = [], size = image_size, show_scroll = "vertical"):
    style_prefix "vertical_menu"
    frame:
        viewport maximum size:

            draggable True
            mousewheel True
            scrollbars show_scroll
            has vbox

            for command in commands:

                if _type == "image":

                    imagebutton auto "test_button0_%s.png" action Jump("test_start")
                    imagebutton auto "test_button1_%s.png" action Jump("test_start")
                    imagebutton auto "test_button2_%s.png" action Jump("test_start")
                elif _type == "text":
                    frame background Frame("xmen_background.png") xminimum size[0]:
                        textbutton [str(command)] action Jump("test_start")
                else:
                    text "PLEASE SPECIFY VERTICAL MENU '_type'"

screen movable_frame(pos):
    drag:
        pos pos

        frame:
            # background Frame("movable_frame.png", 10, 10)
            top_padding 20

            transclude

# Locations Screens
# Show the location bg
screen location_bg_screen(location):
    layer 'background'
    add location.background

# Show the Locations Adjacents
screen location_adjacents(location):
    for adjacent in location.adjacents and not location.accesible:
        textbutton "[adjacent]" action Jump(adjacent.label)
    textbutton "Return" action Return()

# People in a location
screen location_people(location):
    for person in location.people:
        text "[person]"

# Inventory in a location
screen location_inventory(location):
    for item in location.inventory:
        textbutton "[item]" action NullAction() #return a nullaction, add a functionality to items


code:
"""
    01time.rpy

    By: Kelfistan
    Date: 06/20/2022
    Arbitrary variables for in game timer for trainer games filled with period, intervals, and action points
    Creates pacing for the game and other game mechanics
"""

init -900 python in trainer:

    class MissingIntervalError(KeyError):
        pass

    class MissingTimeLayerError(KeyError):
        pass

    class TimeLayer:
        """
        Represents a single layer of time (e.g., Time of Day, Day, Week, etc.).

        Attributes:
            name (str): The name of the time layer.
            intervals (list): The intervals that this time layer cycles through.
            current_index (int): The current index within the interval list.
        """

        def __init__(self, name, intervals):
            """
            Initializes a time layer with given intervals.

            Args:
                name (str): The name of the time layer (e.g., "Time of Day", "Day", "Month").
                intervals (list): The list of intervals for this layer (e.g., ["Morning", "Afternoon", "Evening"]).
            """
            if not isinstance(intervals, list) or not all(isinstance(i, str) for i in intervals):
                raise ValueError("Intervals must be a non-empty list of strings.")

            self._name = name
            self._intervals = intervals
            self._current_index = 0

        @property
        def current_interval(self):
            """Returns the current interval of the layer."""
            return self._intervals[self._current_index]

        @property
        def intervals(self):
            """Returns the intervals of this layer."""
            return self._intervals

        @property
        def name(self):
            """Returns the name of the layer."""
            return self._name

        def increment(self):
            """
            Advances to the next interval in the layer. Returns True if the layer completes and needs to overflow to the next layer.

            Returns:
                bool: True if the layer has completed all intervals and resets to the first.
            """
            self._current_index = (self._current_index + 1) % len(self._intervals)
            return self._current_index == 0  # Returns True if it's completed the cycle

        def reset(self):
            """Resets the layer to the first interval."""
            self._current_index = 0

        def __str__(self):
            """Returns a string representation of the current interval."""
            what = self.current_interval

            if PY2:
                what = what.encode("utf-8")

            return what

        def __format__(self, spec):
            """
            Formats the item as a string according to the given format specification.

            Args:
                spec (str): The format specification.

            Returns:
                str: The formatted string.
            """
            return format(str(self), spec)

        def __repr__(self):
            """
            Provides a string representation of the item for debugging purposes.

            Returns:
                str: A string representation of the item.
            """
            return "<{}: {!r}>".format(self.name, self.__str__())

    time_structure = {
        "Time of Day": ["Morning", "Afternoon", "Evening", "Night"],
        "Day": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
        "Week": ["Week 1", "Week 2", "Week 3", "Week 4"],
        "Month": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
        "Year": ["Year 1", "Year 2", "Year 3", "Year 4"]
    }

    class TimeManager:
        """
        A hierarchical time management system that tracks multiple layers of time.

        Attributes:
            layers (list): A list of TimeLayer objects, each representing a different level of time (e.g., ToD, Day, Week, etc.).
        """

        def __init__(self, time_structure=time_structure):
            """
            Initializes the time system with multiple layers of time.

            Args:
                time_structure (dict): A dictionary representing the layers of time. Keys are layer names, values are interval lists.
                                    Example: {"ToD": ["Morning", "Afternoon", "Evening"], "Day": ["Monday", "Tuesday", ...]}
            """
            self.layers = [TimeLayer(name, intervals) for name, intervals in time_structure.items()]

        def current_time(self):
            """
            Returns a string representing the current time across all layers.

            Returns:
                str: A string representing the current state of all layers.
            """
            return " | ".join(str(layer) for layer in self.layers)

        def increment(self):
            """
            Advances the lowest time layer (e.g., Time of Day). If a layer completes its cycle, it increments the next layer.
            """
            for layer in self.layers:
                if not layer.increment():  # If the layer doesn't need to overflow
                    break  # Stop here, don't increment higher layers

        def reset(self):
            """Resets all time layers to their initial intervals."""
            for layer in self.layers:
                layer.reset()

        def set_time(self, layer_name, interval):
            """
            Sets the current interval for a specific layer.

            Args:
                layer_name (str): The name of the time layer (e.g., "Day", "Time of Day").
                interval (str): The interval to set for the given layer (e.g., "Afternoon").
            """
            for layer in self.layers:
                if layer.name == layer_name:
                    if interval in layer.intervals:
                        layer._current_index = layer.intervals.index(interval)
                        return
                    raise ValueError(f"{interval} is not a valid interval for {layer_name}.")
            raise MissingTimeLayerError(f"Layer {layer_name} not found.")

        def __str__(self):
            """
            Returns a string representation of the current time state.

            Returns:
                str: A string showing all layers and their current intervals.
            """
            return self.current_time()

        def __repr__(self):
            """
            Provides a string representation of the item for debugging purposes.

            Returns:
                str: A string representation of the item.
            """
            return f"<TimeManager: {self.current_time()}>"

init -899 python:
    trainer.time = trainer.TimeManager
    renpy.store.TimeManager = trainer.time
    renpy.store.time = trainer.time # FAQ: time is a very broad variable name -- maybe should be called trn_time or something; easier for debugging


code:
"""
    01wardrobe.rpy
"""
init offset = -99

python early in wardrobe:
    import re
    from store import Null, config, Text, eval
    from collections import OrderedDict, defaultdict

    #region Exceptions
    # Exceptions for logging the Outfit system:
    class OutfitMissingException(Exception): pass

    class MissingWardrobeException(Exception): pass

    #endregion Exceptions

    #region Outfits

    class Outfit(renpy.store.object):
        """
        A saved collection of clothing items.
        """

        def __init__(self, name, clothes={}, kind=None, shame_function=None):
            self._name = name
            self._clothes = clothes
            self._kind = kind
            self._shame_function = shame_function

        def _calculate_shame(self):
            """Calculates a shame value based on items in clothes."""
            if self._shame_function:
                shame = self._shame_function(self)
            else:
                shame = 0
                for clothing in self._clothes.values():
                    shame += clothing.calculate_shame(None)  # No location provided for now
            return shame

        def _calculate_appropriateness(self):
            """Determines if the outfit is appropriate."""
            for clothing in self._clothes.values():
                if not clothing.appropriate(None):  # No location provided for now
                    return False
            return True

        def _calculate_hidden(self):
            """Determines if any body parts are hidden."""
            for clothing in self._clothes.values():
                for part, status in clothing.hides.items():
                    if status['state']:
                        return True
            return False

        def _calculate_covered(self):
            """Determines if any body parts are covered."""
            for clothing in self._clothes.values():
                for part, status in clothing.covers.items():
                    if status['state']:
                        return True
            return False

        @property
        def name(self):
            """The name of the outfit."""
            return self._name

        @property
        def kind(self):
            """The kind of outfit."""
            return self._kind

        @property
        def shame(self):
            """The shame value of the outfit."""
            return self._calculate_shame()

        @property
        def appropriate(self):
            """Indicates whether the outfit is appropriate."""
            return self._calculate_appropriateness()

        @property
        def hidden(self):
            """Indicates whether any body parts are hidden."""
            return self._calculate_hidden()

        @property
        def covered(self):
            """Indicates whether any body parts are covered."""
            return self._calculate_covered()

        def __repr__(self):
            return "<Outfit: {!r}>".format(self._name)

    named_outfit_types = []
    default_outfit = ("uniform", Outfit("default_outfit"))

    #endregion Outfits

    #region Wardrobe

    class Wardrobe(renpy.store.object):
        """ Wardrobe holds and manages the Outfit and Clothes of a Character. """

        wardrobes = {}

        def __init__(self, character, **properties):
            self._character = character
            self._name = self._character._real_name
            self._clothes = properties.get("clothes", []) # Clothes are single wearables
            self._outfits = properties.get("outfits", {}) # Outfits are a combination of clothes to make a set-pieces
            self._clothing_categories = properties.get("clothing_categories", {})

            # Meant as a double check to see that all clothing attire is within the clothings list
            for outfit_name in self._outfits.keys():
                for clothing in self._outfits[outfit_name]:
                    if (clothing not in self._clothes):
                        self._clothes.append(clothing)

            for clothing_type in self._clothing_categories.keys():
                for clothing in self._clothing_categories[clothing_type]:
                    if (clothing not in self._clothes):
                        self._clothes.append(clothing)

            # for kind in named_outfit_types:
            #     setattr(self, kind, properties.get(kind, None))

            # if default_outfit:
            #     setattr(self, default_outfit[0], default_outfit[1])
            #     self._current = default_outfit[0]
            # else:
            #     self._current = None

            Wardrobe.wardrobes[name] = self

        @property
        def clothes(self):
            return self._clothes

        @property
        def current(self):
            return self._current

        @current.setter
        def current(self, outfit):
            if outfit in self._outfits:
                self._current = outfit
            else:
                raise OutfitMissingException

        @property
        def outfits(self):
            all_outfits = {}
            for kind in named_outfit_types:
                all_outfits[kind] = getattr(self,kind)
            return all_outfits

        def add_outfits(self, *outfits):
            return

        def add_clothes(self, *clothes):
            return

        def get_current_group(group):
            '''Returns a list of active attributes for the current clothing group.'''

            l = self._current.get_group(group)
            rv = []

            for c in l:
                rv.extend(c._attributes)

            return rv

        def get_clothes(self, _kind = None):
            '''
            Returns all the clothes inside this wardrobe. Give a string clothing categories as an extra argument for a specific set
            '''
            if (_kind is None):
                for clothing in self._clothes:
                    print(clothing)
                return len(self._clothes)
            else:
                if _kind in self._clothing_categories:
                    for clothing in self._clothing_categories[_kind]:
                        print(clothing)
                    return len(self._clothing_categories[_kind])
                else:
                    print("Please use get_clothes_cats() for a list of all clothing categories")
                    return 0

        def get_outfits(self):
            '''
            Lists all outfits and the associated clothes
            '''
            outfits = self._outfits.keys()
            for outfit_name in outfits:
                print("Outfit name: " + outfit_name)
                for clothing in self._outfits[outfit_name]:
                    print(clothing)
            return len(self._outfits)

        def get_clothes_cats(self):
            '''
            Lists all the different clothing attire such as pants, hats, shirts, panties, socks -- categories should be plural
            '''
            for clothes_cat in self._clothing_categories.keys():
                print(clothes_cat)
            return len(self._clothing_categories.keys())

        @classmethod
        def get_wardrobe(cls, character_name):
            '''
            Class method for getting wardrobe via wardrobe name. Name should be named after the associated character name
            '''
            try:
                return cls.wardrobes[character_name]
            except KeyError as key_error:
                print(f"Wardrobe for {character_name} does not exist")
                print("Please type in 'Wardrobe.get_wardrobes()' for list of all current wardrobes")
                return None

        @classmethod
        def get_wardrobes(cls):
            '''
            List all current wardrobes currently created in the game
            '''
            count = 0
            for wardrobe_name in cls.wardrobes.keys():
                print(wardrobe_name)
                count+=1
            return count

    #endregion Wardrobe

    #region WardrobeCDS

    def parse_outfit(l):
        outfit_name = l.require(l.name)
        outfit_params = {"name": outfit_name}

        outfit_params["kind"] = l.simple_expression()
        outfit_params["character"] = re.split(" ", l.delimited_python(":"))
        l.require(":")
        l.expect_eol()

        set_piece = []
        ll = l.subblock_lexer()
        while ll.advance():
            clothing = ll.name()
            set_piece.append(clothing)
        outfit_params["clothes"] = set_piece
        return outfit_params

    def execute_outfit(outfit_params):
        check =  all(clothes in trainer.Clothing.clothes for clothes in outfit_params["clothes"])
        if check:
            new_outfit = trainer.Outfit(**outfit_params)
            renpy.say("DEVELOPER", new_outfit.props())
        else:
            raise Exception(f"Undefined clothes in {outfit_params['name']} - check spelling and clothing")

    def lint_outfit(o):
        renpy.error(str(o))
        renpy.error(str(trainer.Clothing.clothes))
        return o

    renpy.register_statement("add outfit", parse_outfit, lint_outfit, execute_outfit, block = True)

    def parse_wardrobe(l):
        wardrobe_name = l.require(l.name)
        l.require(":")
        l.expect_eol()

        wardrobe_params = {'name': wardrobe_name, "clothes": [], "outfits": {}, "cloth_categories": {}}

        ll = l.subblock_lexer()

        parameter_to_check = "clothes"
        while ll.advance():
            if (ll.keyword("outfit")):
                outfit_name = ll.require(ll.name)
                wardrobe_params["outfits"][outfit_name] = []
                ll.require(":")
                ll.expect_eol()
                lll = ll.subblock_lexer()
                while lll.advance():
                    wardrobe_params["outfits"][outfit_name].append(lll.word())
            elif (ll.keyword("clothing") or ll.keyword("clothes") or ll.keyword("cloth")):
                ll.require(":")
                ll.expect_eol()
                lll = ll.subblock_lexer()
                while lll.advance():
                    wardrobe_params["clothes"].append(lll.word())
            else:
                if (ll.has_block()):
                    category_name = ll.name()
                    wardrobe_params["cloth_categories"][category_name] = []
                    ll.require(":")
                    ll.expect_eol()
                    lll = ll.subblock_lexer()
                    while lll.advance():
                        wardrobe_params["cloth_categories"][category_name].append(lll.word())
                else:
                    wardrobe_params["clothes"].append(ll.word())

        return wardrobe_params

    def execute_wardrobe(wardrobe):
        try:
            character_wardrobe = Wardrobe.get_wardrobe(wardrobe["name"])
            if (character_wardrobe is None): #NOTE: The Character class is NOT connected to an associated wardrobe...for now
                raise MissingWardrobeException
        except MissingWardrobeException as mwe: #NOTE: Could be a bad habit of making an unwanted Wardrobe // Might just call error warning
            print(mwe)
            print("Wardrobe does not exist yet, making new one now")
            Wardrobe(wardrobe)

    def lint_wardrobe(l):
        return l

    renpy.register_statement("add wardrobe", parse_wardrobe, lint_wardrobe, execute_wardrobe, block = True)

    #endregion WardrobeCDS

    renpy.store.Outfit = Outfit
    renpy.store.Wardrobe = Wardrobe

init -1 python:
    #
    # Developer debug functions
    #

    def get_wardrobe(character_name):
        '''
        Please see Wardrobe.get_wardrobe(cls, character_name)
        '''
        return (Wardrobe.get_wardrobe(character_name))

    def get_wardrobes():
        return (Wardrobe.get_wardrobes())


code:
$ test_menu.add_entry("Location Test", label="test_locations")

label test_locations():
    pass


code:
# Inventory Screen

screen inventory_screen(inventory):
    modal True

    default current_tab = "MC Inventory"
    default selected_item = None
    default current_category = None
    default current_page = 1
    default total_pages = 0
    default screen_size = "medium"
    default show_category_dropdown = False
    default search_query = ""
    default items_per_row = 4
    default grid_height = 2
    default grid_width = 2
    default page_number = 0

    $ screen_width, screen_height = {"small": (800, 600), "medium": (1024, 768), "large": (1280, 960)}[screen_size]

    # Calculate total items per page dynamically
    $ items_per_page = grid_height * grid_width

    # Calculate total pages
    $ total_items = len([item for item in inventory if current_category is None or item.category == current_category])
    $ total_pages = int((total_items + items_per_page - 1) // items_per_page)

    frame:
        style_prefix "inventory"
        xsize screen_width
        ysize screen_height

        vbox:
            # Screen size selection
            hbox:
                text "Screen Size:" style "inventory_text"
                for size in ["small", "medium", "large"]:
                    textbutton size:
                        action SetScreenVariable("screen_size", size)
                        style "inventory_button"

            # Tabs
            hbox:
                textbutton "MC Inventory":
                    action [SetScreenVariable("current_tab", "MC Inventory"), SetScreenVariable("current_category", None), SetScreenVariable("current_page", 1)]
                    style "inventory_button"
                textbutton "Character Inventories":
                    action [SetScreenVariable("current_tab", "Character Inventories"), SetScreenVariable("current_category", None), SetScreenVariable("current_page", 1)]
                    style "inventory_button"
                textbutton "Wardrobe Inventory":
                    action [SetScreenVariable("current_tab", "Wardrobe Inventory"), SetScreenVariable("current_category", None), SetScreenVariable("current_page", 1)]
                    style "inventory_button"

            # Search bar need rework
            # hbox:
            #     default search = ""
            #     default input_search = ScreenVariableInputValue("")
            #     text "Search:" style "inventory_text"
            #     frame:
            #         xsize 500
            #         xmaximum 500
            #         ysize 30
            #         input:
            #             value ScreenVariableInputValue("search") length 10
            #     textbutton "Search":
            #         #action Function(search_items, search)
            #         style "inventory_button"

            # Category dropdown
            if current_tab != "Wardrobe Inventory":
                hbox:
                    text "Category:" style "inventory_text"
                    textbutton "[current_category or 'All']":
                        action ToggleScreenVariable("show_category_dropdown")
                        style "inventory_button"
                        id "category_button"

                if show_category_dropdown:
                    nearrect:
                        focus "category_button"

                        frame:
                            style "inventory_frame"
                            modal True

                            vbox:
                                # FIXME Rework
                                for category in get_categories(current_tab):
                                    textbutton category:
                                        action [SetScreenVariable("current_category", category), SetScreenVariable("current_page", 1), SetScreenVariable("show_category_dropdown", False)]
                                        style "inventory_button"
                                textbutton "All":
                                    action [SetScreenVariable("current_category", None), SetScreenVariable("current_page", 1), SetScreenVariable("show_category_dropdown", False)]
                                    style "inventory_button"

            # Items grid
            # FIXME:
            # Don't show same items, only the count of them
            # TODO:
            # add styles and options to imagebuttons
            # get usages, equipables, gifteables
            frame:
                style "inventory_frame"
                xfill True
                yfill True

                viewport:
                    scrollbars "vertical"
                    mousewheel True

                    grid grid_width grid_height:
                        spacing 5

                        $ start_idx = (current_page - 1) * items_per_page
                        $ end_idx = min(start_idx + items_per_page, total_items)
                        $ displayed_items = [item for item in inventory if current_category is None or item.category == current_category][start_idx:end_idx]

                        for items in displayed_items:
                            button:
                                style "inventory_button"
                                xsize 200
                                ysize 200
                                action NullAction() # get the uses of a item.

                                vbox:
                                    text items.name style "inventory_text" align (0.5, 0.0)
                                    if items.icon:
                                        add items.icon fit "contain" align (0.5, 1.0)

                                    else:
                                        add "missing_image.png" fit "contain" align (0.5, 1.0)
                                    text str(inventory.count(items)):
                                        xalign 1.0
                                        yoffset -30

            # Pagination
            # TODO:
            # proper pagination
            # styles and options
            hbox:
                style_prefix "inventory"
                xalign 0.5
                spacing 20

                textbutton "<<":
                    action SensitiveIf(current_page > 1),SetScreenVariable("current_page", current_page - 1)
                text "Page [current_page] of [total_pages]" style "inventory_text"

                textbutton ">>":
                    action SensitiveIf(current_page < total_pages),SetScreenVariable("current_page", current_page + 1)

            # Grid size controls only to test
            # hbox:
            #     style_prefix "inventory"
            #     xalign 0.5
            #     spacing 20

            #     text "Items per row:"
            #     textbutton "-" action SetScreenVariable("items_per_row", max(1, items_per_row - 1))
            #     text "[items_per_row]"
            #     textbutton "+" action SetScreenVariable("items_per_row", min(6, items_per_row + 1))

            #     null width 50

            #     text "Items per page:"
            #     textbutton "-" action SetScreenVariable("items_per_page", max(1, items_per_page - 3))
            #     text "[items_per_page]"
            #     textbutton "+" action SetScreenVariable("items_per_page", min(18, items_per_page + 3))

    # Close button
    textbutton "Close":
        action Hide("inventory_screen")
        align (0.95, 0.05)
        style "inventory_button"

# Styles
style inventory_frame is frame:
    background "#8f8f8faa"
    padding (20, 20)

style inventory_vbox is vbox:
    spacing 10

style inventory_hbox is hbox:
    spacing 10

style inventory_text is text:
    color "#ffffff"
    size 18

style inventory_button is button:
    idle_background "#000000"
    hover_background "#6a6a6a"
    padding (10, 5)

style inventory_button_text is text:
    color "#ffffff"
    size 16

# Add a separated mc inv screen
screen mc_inventory_screen():
    pass


code:
"""
    02testsuit.rpy

    This file enables the test suite and overrides the start label.
"""

init -1 python:
    # Overrides the default start label to start into the Test Suite instead.
    config.label_overrides = {"start": "test_start"}

init -2 python:

    from store.dynamicentry import DynamicEntry
    from store.location import Location
    from store.layeredimage import LayeredImage

    my_room = Location("My Room", ["Classroom", "University Square", "Classroom", "The Showers"])
    # classroom = Location("Classroom", ["University Square", "The Pool"])
    # university_square = Location("University Square", ["Classroom"], day_cycle=True, variants="halloween")
    # the_pool = Location("The Pool", ["University Square", "Classroom"], day_cycle=True)
    # the_danger_room = Location("The Danger Room", ["University Square"])
    # the_showers = Location("The Showers", ["University Square"])
    # xaviers_study = Location("Xavier's Study", ["University Square"])
    # girls_dormitory = Location("Girl's Dormitory", ["University Square"])
    # test_location = Location("Test Location", ["University Square"])

    player = Character("player", image = "", kind = trn)
    rogue = Character("Rogue", image = "rogue_sprite.png", kind = trn, _layer = 'chr_active')
    rogue2 = Character("Rogue2", image = "rogue_sprite.png", kind = trn, _layer = 'chr_active')
    rogue3 = Character("Rogue3", image = "rogue_sprite_2.png", kind = trn, _layer = 'chr_active')
    rogue4 = Character("Rogue4", image = "rogue_sprite.png", kind = trn, _layer = 'chr_active')

    # university_square.add_person(rogue)
    # university_square.add_person(rogue2)
    # university_square.add_person(rogue3)
    # university_square.add_person(rogue4)

# Creates a dynamic menu for the Test Suite.
default test_menu = DynamicEntry("Test Menu", kind="menu")

# Default start for the Test Suite. Calls the dynamic menu.
menu test_start:
    "Would you like to start the Test Suite?"

    "Yes":
        $ config.developer = True
        jump test_pick

    "No":
        $ del config.label_overrides['start']
        jump start

menu test_pick:
    "You may go to a custom test or use the default suite."
    "Default Suite":
        jump test_suite
    "Custom Test":
        python:
            custom = renpy.input("What is the name of the test label?\nThe label should be in the format \"test_{{name}\".", "suite")
            renpy.jump("_".join(["test",custom]))

label test_suite(de = test_menu):
    #TODO: Create test suite menu via dynamic entries.
    show screen inventory_screen(test_inv)
    jump test_inventory
    pause
    return


code:
# item_test.rpy

default test_item_1 = Item("Test Item 1", kind=renpy.store.item, unique = True, category = ["Test Items", "General"], cost = 100)
default test_item_2 = Item("Test Item 2", category = ["Test Items", "Specific"], status = 'used')

default test_inv = Inventory()

label test_inventory:
    call test_print_inventory
    call test_print_money

    $ test_inv.append(test_item_1)
    $ test_inv.append(test_item_2)

    call test_print_inventory

    $ test_inv.append(50)

    call test_print_money

label test_print_inventory:
    "Current Inventory: [test_inv]"
    return

label test_print_money:
    "Current Money = [test_inv.money]"
    return


code:
init python:
    ## Sets the build directory and naming for the module
    build.destination = "renpy-trainer-dists"
    build.directory_name = "renpy-trainer"

    ## Sets an archive for renpy-trainer
    build.archive("trainer", "module")
    build.archive("testsuite", "test-module")

    ## These control the build of archives
    build.classify("game/_01trainer/**", "trainer")
    build.classify("game/_02testsuite/**", "testsuite")

    ## Adds specific files to packages
    build.classify("game/trainer_*.rpy", "module")

    ## These remove certain files from being added to builds, archives, or packages
    build.classify("**.rpy", None)
    build.classify("**.rpyc", None)
    build.classify("**.md", None)
    build.classify("**.bat", None)
    build.classify("/error.txt", None)
    build.classify("/log.txt", None)
    build.classify("/traceroute.txt", None)
    build.classify("/project.json", None)

    ## This builds the individual packages of the module
    build.package("module", "bare-zip", "module")
    build.package("testsuite", "bare-zip", "test-module")