code:
init offset = -99

python early in emotion:

    class ExpressionController:
        def __init__(self, character, **properties):
            self._character = character
            self._expression_groups = properties.get("groups", [])  # List to hold references to ExpressionGroup objects
            self._max_count = properties.get("max_count", 5)  # Number of previous values to consider for averaging
            self._average_intensity = 0
            self._average_positivity = 0
            self._intensity_values = []
            self._positivity_values = []
            self._reaction_count = 0
            self._reaction_intensity = 0
            self._reaction_positivity = 0

        def add_expression_group(self, group):
            """
            Add an ExpressionGroup object to the controller.

            Args:
                group (ExpressionGroup): The ExpressionGroup object to add.
            """
            self._expression_groups.append(group)

        def add_reaction(self, intensity, positivity):
            """
            Add a reaction to the character's emotion.

            Args:
                intensity (float): The intensity of the reaction.
                positivity (float): The positivity of the reaction.
            """
            self._reaction_intensity += intensity
            self._reaction_positivity += positivity
            self._intensity_values.append(self._reaction_intensity)
            self._positivity_values.append(self._reaction_positivity)
            self._reaction_count += 1

        def update_expression(self):
            """
            Update the character's emotion based on the given positivity and intensity values.
            """
            # Calculate the averages
            self._average_intensity = sum(self._intensity_values) / len(self._intensity_values)
            self._average_positivity = sum(self._positivity_values) / len(self._positivity_values)

            # Update expression groups
            self._display_working_emotion()

        def _apply_expression(self, intensity, positivity):
            """
            Apply the selected expression to the character based on the current intensity and positivity values.
            """
            for group in self._expression_groups:
                group.update_expression(intensity, positivity)

        def _display_current_emotion(self):
            """
            Display the current emotion of the character.
            """
            self.apply_expression(self._reaction_intensity, self._reaction_positivity)

        def _display_working_emotion(self):
            """
            Display the working emotion of the character.
            """
            self.apply_expression(self._average_intensity, self._average_positivity)

        def _trim_reactions(self):
            if len(self._positivity_values) > self._max_count:
                self._positivity_values.pop(0)
            if len(self._intensity_values) > self._max_count:
                self._intensity_values.pop(0)


code:
"""
events.rpy

Houses the Event class and EventHandler class -- used for all forms of actions including dailies, repeatables, interactable events.
EventHandler has one instance and should keep track of ALL events regardless of where they are.
"""

init -1000 python in trainer:
    import json
    import re

    # TODO: Rewrite all execeptions into a seperate file #189
    class EventInvalidKeyError(Exception):
        """
        Exception raised when the key is not an instance of a string.
        """
        pass

    class EmptyConditionsExceptions(Exception):
        """
        Exception raised when there are no conditions in this event to meet requirements.
        """
        pass

    class SameEventName(Exception):
        """
        Exception raised when event names/labels must be different from one another.
        """
        pass

    class Event(renpy.store.object):
        """
        Event object that stores all the possible variables needed for all types of events and the required specifications.

        :param label: The label of the event.
        :type label: str
        :param conditions: The conditions for the event.
        :type conditions: list
        :param trigger: The trigger for the event.
        :type trigger: str
        :param kwargs: Additional keyword arguments for optional parameters.
        """

        def __init__(self, label, conditions, trigger, **kwargs):
            '''
            Event object that stores all the possible variables needed for all types of events and the required specifications
            '''
            self._label = label
            self._conditions = conditions
            self._cleared_conditions = [True] if (len(conditions) <= 0) else [False for i in range(len(conditions))]
            self._trigger = trigger
            self._id = None

            # Optional
            self._static = kwargs.get("static", False)
            self._repeatable = kwargs.get("repeatable", False)
            self._random = kwargs.get("random", False)
            self._weight = int(kwargs.get("weight", 0))
            self._weight_mod = 0
            self._special = kwargs.get("special", False)
            self._active = kwargs.get("active", False)
            self._complete = False
            self._interactable = kwargs.get("interactable", False)
            self._button_text = kwargs.get("button_text", None)
            self._incr_time = int(kwargs.get("incr_time", 0))
            if self._button_text is None:
                self._button_text = re.sub("_"," ", self._label).capitalize()
            self._tags = kwargs.get("event_tags", ['all'])

        def __str__(self):
            """
            Returns a string representation of the Event object.
            """
            return f"Event object {self._label}, event_tags: {self.tags}, conditions: {self.conditions}, trigger: {self.trigger}"

        @property
        def label(self):
            """
            :return: The label of the event.
            :rtype: str
            """
            return self._label

        @property
        def button_text(self):
            """
            :return: The button text of the event.
            :rtype: str
            """
            return self._button_text

        @property
        def weight(self):
            """
            :return: The weight of the event.
            :rtype: int
            """
            return self._weight + self._weight_mod

        @property
        def weight_mod(self):
            """
            :return: The weight modifier of the event.
            :rtype: int
            """
            return self._weight_mod

        @property
        def incr_time(self):
            """
            :return: The time increment of the event.
            :rtype: int
            """
            return self._incr_time

        @weight_mod.setter
        def weight_mod(self, mod):
            """
            Sets the weight modifier for the event.

            :param mod: The weight modifier.
            :type mod: int
            """
            self._weight_mod = mod

        @property
        def id(self):
            """
            :return: The ID of the event.
            :rtype: int
            """
            return self._id

        @id.setter
        def id(self, id):
            """
            Sets the ID for the event.

            :param id: The ID of the event.
            :type id: int
            """
            self._id = id

        @property
        def tags(self):
            """
            :return: The tags of the event.
            :rtype: list
            """
            return self._tags

        @tags.setter
        def tags(self, tags):
            """
            Sets the tags for the event.

            :param tags: The tags of the event.
            :type tags: list
            """
            self._tags = tags

        def is_static(self):
            """
            :return: Whether the event is static.
            :rtype: bool
            """
            return self._static

        def is_interactable(self):
            """
            :return: Whether the event is interactable.
            :rtype: bool
            """
            return self._interactable

        def is_repeatable(self):
            """
            :return: Whether the event is repeatable.
            :rtype: bool
            """
            return self._repeatable

        def is_random(self):
            """
            :return: Whether the event is random.
            :rtype: bool
            """
            return self._random

        def is_special(self):
            """
            :return: Whether the event is special.
            :rtype: bool
            """
            return self._special

        def is_active(self):
            """
            :return: Whether the event is active.
            :rtype: bool
            """
            return self._active

        def set_active(self, active):
            """
            Sets the active status for the event.

            :param active: The active status.
            :type active: bool
            """
            self._active = active

        def is_complete(self):
            """
            :return: Whether the event is complete.
            :rtype: bool
            """
            return self._complete

        def set_complete(self, com):
            """
            Sets the complete status for the event.

            :param com: The complete status.
            :type com: bool
            """
            self._complete = com

        def update(self):
            """
            Iterates through all event conditions and checks if they have been cleared.
            """
            for i, condition in enumerate(self._conditions):
                if self._cleared_conditions[i]: continue
                if check_condition(condition): self._cleared_conditions[i] = True

        def is_triggered(self):
            """
            Checks if the event has been triggered.

            :return: Whether the event is triggered.
            :rtype: bool
            """
            return all(self._conditions) and check_condition(self._trigger)

    def check_condition(condition):
        """
        Helper function that evaluates the conditions of the events if conditions are proper
        # TODO: Have a creator defined statement that helps creates conditions easier
        """
        try:
            return eval(condition)
        except SyntaxError as synErr:
            renpy_log.warning("You made a improper expression, look at the operators and variables to see if you structured properly")
            renpy_log.warning(str(synErr) + " in " + str(condition))
            return False
        except Exception as err:
            renpy_log.warning("Something went wrong updating event when checking condition")
            renpy_log.warning(str(err) + " in " + str(condition))
            return False

    class EventHandler(renpy.store.object):
        """
        Handles (updates) and collects all events created
        """

        DEFAULT_WEIGHT = 25

        def __init__(self):
            self.__events = {}
            self.__active_events = {}
            self.__event_names = {}
            self.__queue_events = []

        def get_interact_events(self,tag):
            """
            Returns a list of events based on a tag

            :return: A list of events
            :rtype: list
            """
            interact_events = []

            for evnt in self.__events[tag]:
                if evnt.is_interactable():
                    if evnt.is_triggered():
                        interact_events.append((evnt.button_text, (evnt, "event")))
            return interact_events

        def calculate_weighted_events(self, random_events):
            """
            Algorithem used to pick event based on event weights

            :param random_events:
            :type random_events:

            :return: An event random selected by weight
            :rtype: A
            """
            if len(random_events) <= 0: return None
            tiers = {self.DEFAULT_WEIGHT: None}
            for evnt in random_events:
                if evnt.weight in tiers.keys():
                    tiers[int(evnt.weight)].append(evnt)
                else:
                    tiers[int(evnt.weight)] = [evnt]
            total_weight = 0
            for weight in tiers.keys():
                total_weight = total_weight + weight
            roll = renpy.random.randint(0, total_weight - 1)
            running_total = 0
            tier_keys = list(tiers.keys())
            tier_keys.sort()
            for i in range(len(tier_keys)):
                running_total = running_total + tier_keys[i]
                if roll < running_total:
                    rdm_evnt = renpy.random.choice(tiers[tier_keys[i]])
                    return (rdm_evnt)

        def update_events(self, *tags):
            """
            updates certain events based on a tag

            :param tags: A list of "tags" for key arguments
            :type tags: str
            """
            # renpy_log.info(tags)
            # renpy_log.info( self.__events.keys())
            for event_tag in tags:
                if event_tag in self.__event_names:
                    self.__event_names[event_tag].update()
                    continue
                if event_tag not in self.__events.keys():
                    continue
                for evnt in self.__events[event_tag]:
                    evnt.update()
                    renpy_log.update
                    if evnt.is_triggered():
                        self.__queue_events.append(evnt)

        def play_triggered_events(self):
            """
            Plays the assorted events after there conditions have been met and the trigger is set
            """
            events_to_play = []
            random_events = []
            for evnt in self.__queue_events:
                if evnt.is_random():
                    random_events.append(evnt)

            rdm_evnt = self.calculate_weighted_events(random_events)

            if rdm_evnt is not None:
                random_events.remove(rdm_evnt)
                for evnt in random_events:
                    evnt.weight_mod = (evnt.weight_mod + 5)
                rdm_evnt.weight_mod = 0
                play_event(rdm_evnt)

            self.__queue_events.clear()

        def add_event(self,event):
            """
            Function for sorting and filtering a newly created even into the eventhandler

            :param event: The event object to be added.
            :type event: Event
            """
            #TODO look out for events with duplicate names
            self.__event_names[event.label] = event
            for event_tag in event.tags:
                if event_tag in self.__events.keys():
                    self.__events[event_tag].append(event)
                else:
                    self.__events[event_tag] = [event]

    def play_event(evnt):
        """
        Global function that will play the event.
        The event should be its own designated label to call to.

        :param evnt: Event object to be played.
        :type evnt: Event
        """
        if renpy.has_label(evnt.label):
            game_clock.advance_time(actions = evnt.incr_time)
            # MUST BE LAST IN THE CODE LINE! Terminates any othe code line because control will be given to next statement
            renpy.call(evnt.label)
        else:
            renpy.call("no_event_label", evnt.label)
            renpy_log.info("Please change you ALSO created a label with dialouge with the same name as the event!")

    #### INITIALIZE EVENT_HANDLER
    renpy.store.event_handler = EventHandler()

    renpy.store.TEvent = Event
    renpy.store.TEventHandler = EventHandler
    renpy.store.play_tevent = play_event

init python early:
    import re

    def check_event_params(l, event_params):
        """
        Sorts the event parameters into boolean, string, or integer based
        # TODO: Reworked into a high level wrapper attributes

        :param event_params: A hashmap to store all the parameters.
        :type event_params: dictionary
        """
        event_params["conditions"] = ["True"]
        event_params["trigger"] = "True"
        while l.advance():

            for param in ("interactable", "static", "active", "repeatable", "random"):
                # set_bool_param(l, event_params,param)
                pass

            for param in ("button_text","trigger"):
                # set_string_param(l, event_params,param)
                pass

            for param in ("weight", "incr_time"):
                # set_int_param(l, event_params, param)
                pass

            if l.keyword("event_tags"):
                tags = l.rest()
                tag_list = re.split(" ", tags)
                event_params["event_tags"] = tag_list

            if l.keyword('conditions'):
                ll = l.subblock_lexer()
                # Not really satisfied with nested while loop; will try to think of an alternative
                while(ll.advance()):
                    # description
                    # if ll.keyword("description"):

                    event_params['conditions'].append(ll.simple_expression())

        return event_params

    def parse_event(l):
        """
        Interprets the event parameters from the CDS

        :param l: The lexer object that contains interpretable data.
        :type l: lexer
        """
        name = l.require(l.name)
        l.require(":")
        l.expect_eol()
        event_params = {}
        _label = re.sub('[ ]','_',re.sub('[()[\]{}+!@#$%^&*-]','',name)).lower()
        event_params["label"] = _label
        l.expect_block(f"Event block {_label} requires properties")
        return check_event_params(l.subblock_lexer(), event_params)

    def execute_event(event_params):
        """
        Generates the event objects and places it into the Event Handler

        :param event_params: A hashmap to store all the parameters.
        :type event_params: dictionary
        """
        try:
            new_event = trainer.Event(**event_params)
            renpy.store.event_handler.add_event(new_event)
        except Exception as err:
            renpy_log.warning("********************************* ERROR EXCEPTION HANDLING 'execute_update_event *********************************")
            renpy_log.warning(err)
            renpy_log.warning(f"event_parameters -- {event_params}")
            renpy_log.warning("********************************* ERROR EXCEPTION HANDLING 'execute_update_event *********************************")

    def lint_event(event_params):
        """
        Error checking for events
        # TODO: Improve error checking on event addition

        :param event_params: A hashmap to store all the parameters.
        :type event_params: dictionary
        """
        # try:
            # event_params["label"]
            # if len(event_params["conditions"]) <= 0:
            #     raise EmptyConditionsExceptions
            # checks for conditions
            # for condition in event_params["conditions"]:
            #     renpy.try_eval("Creating An Event", conditions)
            #     eval(condition)
            # checks for trigger
            # eval(event_params["trigger"])

        # except EmptyConditionsExceptions as err:
        #     renpy.error("Please add conditions to the event!")
        #     renpy_log.warning("Please add conditions to the event!")
        #     event_params["conditions"].append(True)
        # except Exception as err:
        #     renpy.error("Please fix the conditions -- non-empty string or proper expressions")
        #     renpy_log.warning("Please fix the conditions -- non-empty string or proper expressions")
        #     renpy_log.warning(err)
        return

    renpy.register_statement("add event", parse = parse_event, lint = lint_event, execute = execute_event, block = True, )

    def parse_condition(l):
        """
        Will interprete whether the conditions of the events are logical

        :param l: The lexer object that contains interpretable data.
        :type l: lexer
        """
        sl = l.subblock_lexer()
        py_expressions = []
        while sl.advance():
            py_expressions.append(sl.simple_expression())
            # py_expressions.append(sl.string())

        return py_expressions

    def execute_condition(o):
        """
        Generates in the conditions inside the event objects

        :param o: The condition object that contains interpretable data.
        :type o: python expression
        """
        renpy_log.info(f"Renpy conditions: {o}")
        string_manipe = []
        for exp in o:
            # new_exp = re.sub("\W+| in | or | not | is | is not | not in | and | == | != | < | > | <= | >= ",',',exp)
            new_exp = re.sub("( and )|( in )|( not )|( or )|( is )|\W+",' ',exp)
            renpy_log.info(f"Object type: {exp}")
            renpy_log.info(f"FIXED Object type: {new_exp}")
            string_manipe.append(new_exp.split())

        renpy_log.info(f"String Manip: {string_manipe}")
    def l_cond(o):
        """
        Linter for conditions
        """
        pass

    renpy.register_statement("py_cond", parse = parse_condition, lint = l_cond, execute = execute_condition, block = True)

    # EVENT UPDATE CCS (specific)

    def parse_event_update(l):
        """
        Event tag formater

        :param l: Lexer object that contains event update commands
        :type l: lexer
        """
        event_tags = re.split(" ", l.rest())
        return event_tags

    def execute_update_event(tags):
        """
        Updates certain events based on the tags

        :param tags: Tag for key arguments
            :type tags: str
        """
        global event_handler
        event_handler.update_events(tags)

    def lint_update_event(tags):
        """
        Linter for upddating events based on tags

        :param tags: Tag for key arguments
        :type tags: str
        """
        pass

    renpy.register_statement("update events", parse = parse_event_update, execute = execute_update_event, lint = lint_update_event)

repo_name: pervy/rogues

license: unknown


code:
"""
    01helpers.rpy

    This file contains globally accessible helper functions.
"""

init -2 python in trainer:

    ######################################################
    # Sex acts functions:
    ######################################################

    def has_available_sex_act(who, sex_act):
        return sex_act in who.available_sex_acts

    def add_available_sex_act(who, sex_act):
        if sex_act not in who.available_sex_acts:
            who.available_sex_acts.append(sex_act)

    def remove_available_sex_act(who, sex_act):
        if sex_act in who.available_sex_acts:
            who.available_sex_acts.remove(sex_act)

    ######################################################
    # Sex counter functions:
    ######################################################

    # ???: those "*_sex_act" looks kinda bad, but i don't know how it's better to shorten it.
    def can_perform_sex_act(who, sex_act):
        return sex_act in who.sex_counter

    def get_count_per_sex_act(who, sex_act):
        return who.sex_counter[sex_act]

    def add_sex_act_count(who, sex_act, increment=1):
        who.sex_counter[sex_act] += increment

    ######################################################
    # Inventory functions:
    ######################################################

    def add_items(entity, items):
        if not isinstance(items, list):
            items = [items]

        for item in items:
            entity.add_item(item)

    def remove_items(entity, items):
        if not isinstance(items, list):
            items = [items]

        for item in items:
            try:
                entity.remove_item(item)
            except:
                renpy.log(f"{entity} doesn't have an {item}")

    def has_items(entity, items):
        if not isinstance(items, list):
            items = [items]

        # ???: Is this logic correct or should it return status of each item separated?
        for item in items:
            if not entity.has_item(item):
                return False

        return True

    def get_items(entity):
        return entity.inventory

    def move_items(entity_from, entity_to, items):
        if has_items(entity_from, items):
            add_items(entity_to, items)
            remove_items(entity_from, items)
        else:
            # Would do something later (probably?).
            pass

    ######################################################
    # Flags functions:
    ######################################################

    def add_flag(entities, flag):
        if not isinstance(entities, list):
            entities = [entities]

        for entity in entities:
            if not is_flag_present(entity, flag):
                entity.add_flag(flag)

    def remove_flag(entities, flag):
        if not isinstance(entities, list):
            entities = [entities]

        for entity in entities:
            if is_flag_present(entity, flag):
                entity.remove_flag(flag)

    def is_flag_present(entities, flag):
        if not isinstance(entities, list):
            entities = [entities]

        for entity in entities:
            if not entity.is_flag_present(flag):
                return False

        return True

    def is_flag_present(entities, flag):
        if not isinstance(entities, list):
            entities = [entities]

        for entity in entities:
            if not entity.is_flag_present(flag):
                return False

        return True

    ######################################################
    # Character functions:
    ######################################################

    def trait_comparison(who, trait_1, trait_2):
        if getattr(who, trait_1) >= trait_2:
            return True
        else:
            return False

    def petname_check(who, petname):
        petname_info = store.self_petnames[petname]
        if trait_comparison(who, petname_info[0], petname_info[1]):
            return True
        else:
            return False

    def relationship_check(who, relationship):
        relationship_info = store.relationship_types[relationship]
        if trait_comparison(who, relationship_info[0], relationship_info[1]):
            return True
        else:
            return False

    def party_check(who, following, trait, requirement):
        """
        Function stub to check if character actually wants to follow someone else
        other characters may form their parties without any trait requirements (because they simply dont exist between other characters)
        """
        if trait == "no trait provided":
            # check in case if party is being created between other characters
            #TODO: change "mc" to actual main character
            #TODO: maybe add relationship requirement for this case
            if following is not mc:
                return True
            else:
                return False
        else:
            if trait_comparison(who, trait, requirement):
                return True
            else:
                return False

    ######################################################
    # Menu functions:
    ######################################################

    menu_set = list(set(renpy.store.dynamicentry.menu_list))

    def fetch_entry(dynamic_entry, entry_list = menu_set):
        """ This function finds a dynamic_entry and returns it """

        de = dynamic_entry
        try:
            if not isinstance(de, DynamicEntry):
                de = [entry for entry in menu_list if entry.text == dynamic_entry]
            return de
        except ValueError:
            return False

    def build_menu(dynamic_entry):
        """ Builds a menu from the supplied Dynamic Entry node.

        Args:
            dynamic_entry: An object of DynamicEntry.

        Returns:
            list: A list of Dynamic_Entry objects.
        """
        menu_list = []

        for child in dynamic_entry.children:
            # Checks to condition to show menu entry.
            if child.condition is None or (child.condition is not None and renpy.python.py_eval(child.condition)):
                menu_list.append(child)

        return menu_list

    ######################################################
    # Party / Follower functions:
    ######################################################

    def follow(who, following, trait="no trait provided", requirement=-1):
        """ Causes a given character to follow another.

            Returns:
                bool: True/False on success
        """

        if party_check(who, following, trait, requirement) and (who.follow is not following):
            who.follow = following
            following.party.append(who)
            return True
        else:
            return False

    def is_following(who, follower):
        """ Checks if a given character is following another character.

            Returns:
                bool: is following
        """
        if who in following.party:
            return True
        else:
            return False

    def stop_following(who):
        """ Causes a given character to stop following another. """
        #TODO: Resolve if this will work.
        who.following.party.remove(who)
        who.following = None
        return

    def move_party_to_location(party, location):
        """ Moves a given list of characters to a specified location. """
        for who in party:
            if who.location is not location:
                who.move(location)
        return

    def remove_location_check(who, relationship):
        """ Checks if the given character will accept a command to move to a different location.

            Returns:
                bool: if relationship is high enough
        """
        if relationship_check(who, relationship):
            return True
        else:
            return False

    def remove_from_location(who, destination):
        """ Removes a character from a location if a relationship check is passed.

            Returns:
                bool: if character is removed
        """
        if remove_location_check(who):
            stop_following(who)
            who.move(who, destination)
            return True
        else:
            return False

    ######################################################
    # Test functions to treat nested lists like dicts
    ######################################################

    def give_contains(list, searchable):
        for x in list:
            if x[0] == searchable:
                return x[1]

    def give_index(list, searchable):
        for idx, x in enumerate(list):
            if x[0] == searchable:
                return idx


code:
"""
    01locations.rpy

    Location Class allows for quick creation of new locations.
"""
python early in location:
    from store.dynamicentry import DynamicEntry
    import re

    class Location(DynamicEntry):
        """
        Represents a location in the game.

        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 properly formated form
            accessible (bool): Indicates if the location is accessible to NPCs.
            adjacents (list): List of adjacent locations.
            background (str): The name of the background image based on the time of day.
            inventory (list): The inventory of the location.
            label (str): The associated label for the location.
            people (list): List of people at the location.
        """

        def __init__(self, name, adjacent, **properties):
            # Name and trimmed name
            self._trimmed = re.sub('[ ]', '_', re.sub("[']", '', name)).lower()  # Used for variable name, code reference

            # Adjacent locations
            self._adjacent_names = adjacent or []
            self._adjacents = []

            # People and location attributes
            self._inventory = properties.pop('inventory', [])
            self._accessible = properties.pop('accessible', True)  # Indicates if the location is accessible to NPCs
            self._path = properties.pop('path', "/images/backgrounds/")
            self._people = []
            self._public = properties.pop('public', 100)

            # Location background images
            # self._day_cycle = properties.pop('day_cycle', False)
            # if self._day_cycle:
            #     renpy.image("bg {}_{}".format(self._trimmed, "day"), "{}{}_{}.png".format(self._path, self._trimmed, "day"))
            #     renpy.image("bg {}_{}".format(self._trimmed, "evening"), "{}{}_{}.png".format(self._path, self._trimmed, "evening"))
            #     renpy.image("bg {}_{}".format(self._trimmed, "night"), "{}{}_{}.png".format(self._path, self._trimmed, "night"))
            # else:
            #     renpy.image("bg {}".format(self._trimmed), "{}{}.png".format(self._path, self._trimmed))

            # Background variants
            self.variants = properties.pop('variants', [])
            if self.variants:
                for variant in self.variants:
                    renpy.image("bg {}_{}".format(self._trimmed, variant), "{}{}_{}.png".format(self._path, self._trimmed, variant))

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

            # Create adjacency relationships
            for adjacent_name in self._adjacent_names:
                self.add_adjacent(adjacent_name)

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

            Returns:
                str: name of location
            """
            return self._trimmed

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

            Returns:
                bool: True if the location is accessible to NPCs, False otherwise.
            """
            return self._accessible

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

            Returns:
                list: List of adjacent locations.
            """
            return self._adjacents

        @property
        def background(self):
            """
            Gets the background image based on the time of day.

            Returns:
                str: The name of the background image.
            """
            if self._day_cycle:
                return "bg {}_{}".format(self._trimmed, current_time.lower())
            else:
                return "bg {}".format(self._trimmed)

        @property
        def inventory(self):
            """
            Gets the inventory of the location.

            Returns:
                list: The inventory of the location.
            """
            return self._inventory

        @property
        def label(self):
            """
            Gets the associated label for the location.

            Returns:
                str: The associated label for the location.
            """
            return self._trimmed

        @property
        def people(self):
            """
            Gets the people at the location.

            Returns:
                list: List of people at the location.
            """
            return self._people

        def add_adjacent(self, adjacent_name):
            """
            Adds an adjacent location to the current location.

            Args:
                adjacent_name (str): The name of the adjacent location.
            """
            # Ensure the adjacent location exists in the menu list
            if adjacent_name in self._menu_list:
                adjacent_location = self._menu_list[adjacent_name]
                self.add_child(adjacent_location)
                adjacent_location.add_child(self)
                self._adjacents.append(adjacent_location)

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

            Args:
                item: The item to add.
            """
            self._inventory.append(item)

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

            Args:
                person: The person to add.
            """
            self._people.append(person)

        def clear_location(self):
            """Clears people from the location."""
            self._people.clear()

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

            Args:
                item: The item to check.

            Returns:
                bool: True if the item is at the location, False otherwise.
            """
            return item in self._inventory

        def has_person(self, person):
            """
            Checks if a person is at the location.

            Args:
                person: The person to check.

            Returns:
                bool: True if the person is at the location, False otherwise.
            """
            return person in self._people

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

            Args:
                item: The item to remove.
            """
            self._inventory.remove(item)

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

            Args:
                person: The person to remove.
            """
            self._people.remove(person)

    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 __init__(self):

            self._locations = {}

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

        def remove_locaton(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 "Unable to get the specified location, please make sure " + str(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()


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 *

    #TODO: Remove after testing.
    clothing_price = 0

    #region Exceptions

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

    class ClothingMissingException(Exception): pass

    class PoseMissingException(Exception): pass

    #endregion Exceptions

    def shame_calculation(appropriate, covers, hides, shame):
        return shame

    #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 Handlers

    class Clothing(renpy.store.object):
        """
        Represents a piece of clothing or accessory.

        Clothing manages clothing attributes and their properties, including hiding and covering body parts,
        appropriateness for specific locations, and calculating shame values.

        Attributes:
            name (str): The name of the clothing.
            category (str): The category this clothing belongs to.
            attributes (list): List of ClothingAttributes controlled by this clothing.
            kind (str): The type of clothing or accessory.
            price (int): The price of the clothing.
            shame (int): The shame value associated with wearing this clothing.
            hides (dict): Dictionary of body parts this clothing hides.
            covers (dict): Dictionary of body parts this clothing covers.
            states (list): List of possible states this clothing can be in.
            poses (list): List of poses this clothing can be worn in.
            location_types (list): List of location types where this clothing is appropriate.

        Methods:
            __init__: Initializes a Clothing object.
            add_attribute: Adds a ClothingAttribute to the clothing.
            appropriate: Checks if the clothing is appropriate for a location.
            calculate_shame: Calculates the shame value based on location and clothing attributes.
            is_covering: Checks if a specific body part is covered by the clothing.
            is_hiding: Checks if a specific body part is hidden by the clothing.
            _update_hides_and_covers: Updates hides and covers based on the current state of the clothing.
        """

        def __init__(self, name, category, attributes=[], shame_calculation=None, **properties):
            self._name = name
            self._category = category
            self._attributes = attributes
            self._kind = properties.get("kind", None)
            self._price = properties.get("price", 0)
            self._shame = properties.get("shame", 0)
            self._hides = {}  # Dictionary of body parts this clothing hides
            self._covers = {}  # Dictionary of body parts this clothing covers
            self._states = properties.get("states", [])
            self._poses = properties.get("poses", ["default"])
            self._location_types = properties.get("location_types", [])
            self._current_state = None
            self._shame_calculation = shame_calculation

            # Initialize hides and covers
            self._update_hides_and_covers()

        @property
        def covers(self):
            """
            Gets the body parts this clothing covers.

            Returns:
                dict: Dictionary of body parts and their cover status.
            """
            return self._covers

        @property
        def current_state(self):
            """
            Gets the current state of the clothing.

            Returns:
                str: The current state of the clothing.
            """
            return self._current_state

        @current_state.setter
        def current_state(self, state):
            """
            Sets the state of the clothing.

            Args:
                state (str): The state to set.
            """
            if state in self.states:
                self._current_state = state
                self._update_hides_and_covers()
            else:
                raise ValueError(f"Invalid state: {state}")

        @property
        def hides(self):
            """
            Gets the body parts this clothing hides.

            Returns:
                dict: Dictionary of body parts and their hide status.
            """
            return self._hides

        def add_attribute(self, attribute):
            """
            Adds a ClothingAttribute to the clothing.

            Args:
                attribute (ClothingAttribute): The ClothingAttribute to add.
            """
            self._attributes.append(attribute)

        def appropriate(self, location):
            """
            Checks if the clothing is appropriate for a location.

            Args:
                location (Location): The location to check.

            Returns:
                bool: True if the clothing is appropriate, False otherwise.
            """
            return location.kind in self._location_types

        def calculate_shame(self, location):
            """
            Calculates the shame value based on location and clothing attributes.

            Args:
                location (Location): The location where the clothing is worn.

            Returns:
                int: The shame value for wearing this clothing in the specified location.
            """
            if self._shame_calculation:
                return self._shame_calculation(
                    self.appropriate(location),
                    self._covers,
                    self._hides,
                    self._shame
                )
            else:
                return self._shame

        def is_covering(self, part):
            """
            Checks if a specific body part is covered by the clothing.

            Args:
                part (str): The body part to check.

            Returns:
                bool: True if the body part is covered, False otherwise.
            """
            return self._covers.get(part, {}).get('state', False)

        def is_hiding(self, part):
            """
            Checks if a specific body part is hidden by the clothing.

            Args:
                part (str): The body part to check.

            Returns:
                bool: True if the body part is hidden, False otherwise.
            """
            return self._hides.get(part, {}).get('state', False)

        def _update_hides_and_covers(self):
            """
            Updates hides and covers based on the current state of the clothing.
            """
            # Logic to update hides and covers based on the current state
            pass

    #endregion Handler
    #region ClothingCDS

    def parse_clothing(l):
        clothing_name = l.require(l.name)
        layer = l.simple_expression()
        layer = 0 if layer is None else int(layer)

        clothing_params = {"name": clothing_name, "layer": layer }

        if l.has_block():
            l.require(":")
            ll = l.subblock_lexer()
            while ll.advance():
                for param in ("layer", "price", "shame"):
                    #set_int_param(ll, clothing_params, param)
                    pass

                for param in ("kind", "image"):
                    #set_string_param(ll, clothing_params, param)
                    pass

                for param in ("hides", "covers"):
                    #set_bool_param(ll, clothing_params, param)
                    pass

        return clothing_params

    def execute_clothing(clothing_params):
        # new_clothing = trainer.Clothing(**clothing_params)
        renpy.say("DEVELOPER", new_clothing.props())

    def lint_clothing(o):
        new_clothing = trainer.Clothing(**o)
        renpy.error(new_clothing.props())
        return o

    renpy.register_statement("add clothing", parse_clothing, lint_clothing, execute_clothing, block = "possible")
    renpy.register_statement("add clo", parse_clothing, lint_clothing, execute_clothing, block = "possible")

    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_attribute(ll, rv)
                ll.advance()

            elif ll.keyword('group'):

                parse_group(ll, rv, name)
                ll.advance()

            elif ll.keyword('if'):

                parse_conditions(ll, rv)
                # Advances for us.

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

            elif ll.keyword('clothing'):
                parse_clothing_group(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.Clothing = Clothing
    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


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 TRNTime(renpy.store.object):
        """
        TODO: Document
        """
        DEFAULT_TIME_INTERVALS = ("MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY")

        def __init__(self, time_intervals):
            """
            Initializes the main scheduler class that Trainer Characters, Locations, and etc.. follow

            :param time_intervals: see @change method
            :type time_intervals: list
            """

            # I know how I can set a child, but how can I access the childs????
            self._child = None
            self.change_intervals(time_intervals)

        def change_intervals(self, time_intervals):
            """
            Sets the current sections of time

            :param time_intervals: A set of time segmentations to track a current section of time progress
            :type time_interval: list
            """
            try:
                if (time_intervals is None ) or not isinstance(time_intervals, list):
                    raise ValueError()
                else:
                    self._time_intervals = time_intervals # FAQ: Maybe make this a set to stop redundancies?
                    self._time_length = len(time_intervals)

                    if not all(isinstance(interval, str) for interval in self._time_intervals):
                        raise TypeError()

                    if (self._time_length <= 0):
                        raise IndexError()

            except TypeError as typError:
                renpy.log("Have all time intervals as strings please")
                self._set_defaults()

            except IndexError as indError:
                renpy.log("Please give a non-empty amount of time intervals")
                self._set_defaults()

            except ValueError as valError:
                renpy.log("Time interval must be a list of time sections such as ['Spring', 'Summer', 'Autumn', 'Winter']")
                self._set_defaults()

            self._time_index = 0

        def _set_defaults(self):
            """
            Sets time intervals into example defaults (see class above)
            """
            self._time_intervals = TRNTime.DEFAULT_TIME_INTERVALS
            self._time_length = 7

        @property
        def interval(self):
            """
            :return: The current time interval.
            :rtype: str
            """
            return self._time_intervals[self._time_index]

        @property
        def num(self):
            """
            :return: The number of time segments there are in total.
            :rtype: int
            """
            return self._time_length

        @property
        def intervals(self):
            """
            :return: The currently set time intervals
            :rtype: list
            """
            return self._time_intervals

        def inc(self, action = None):
            """
            Increments the time by one (Monday --> Tuesday).
            If increments a certain set of time base on the amount of time the action creates

            :param action: If not None, must inherit from TimeAction to get the allotted time
            :type action: TimeAction
            """
            if (action is not None) and (isinstance(action, TimeAction)):
                self._time_index = (self._time_index + action.time) % self._time_index

            self._time_index = (self._time_index + 1 ) % self._time_length

        def dec(self):
            """
            Decrements the time by one (Tuesday --> Tuesday). Should be used sparringly as "time" doesn't tend to go forward
            """
            self._time_index = (self._time_index - 1 ) % self._time_length

        def reset(self):
            """
            Reverts time to start or origin
            """
            self._time_index = 0

        def set(self, interval):
            """
            Sets the current time to the specified date

            :param interval: The appropriate time section indicate via by name or number
            :type interval: int or str
            """

            if isinstance(interval, int):
                if (interval < 0) or (interval > self._time_length):
                    return "The time index given was not within range of the time intervals: " + str(self._time_length)
                self._time_index = interval
                return "Time set to " + str(self.time)

            if isinstance(interval, str):
                count = 0
                for segment in self._time_intervals:
                    if interval == segment:
                        self._time_index = count
                        return "Time set to " + str(self.time)
                return "There is no interval with the name " + interval

            renpy.log("Please specify the name of the time segment you want or specify a number")
            return "Please specify the name of the time segment you want or specify a number"

    def update_time():
        store.time.update

    # class TimeAction(renpy.store.object):

    #     def __init__(self, action_amount, calculate_amount = None):
    #         self._calculate_amount = calculate_amount
    #         self._action_amount = action_amount

    #     @property
    #     def action(self):
    #         if self._calculate_amount is not None:
    #             return self._calculate_amount()
    #         return self._action_amount

    # class TimeUnit(renpy.store.object)

init -899 python:
    trainer.time = trainer.TRNTime([])
    renpy.store.TRNTime = 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
    from store.paperdoll import *

    #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:
init offset = -1

# Location Screen
# TODO: handle  dynamic menus, hide, and characters in place
# TODO: change the name of the screen when added the dynamic menu compatibility
screen dynamic_location_test(location):
    use location_bg_screen(location)
    frame:
        xalign 1.0
        has hbox
        use location_inventory(location)
        use location_people(location)

# Keep it separate to change the zorder or the style of the bg
screen location_bg_screen(location):
    layer 'background'
    add location.background

screen location_menu(location):
    vbox:
        label location.name
        if not location.accessible:
            text "This location is not accessible." color "#ff0000"
        text "Adjacent Locations" color "#48ff00"
        for adjacent in location.adjacents:
            textbutton "[adjacent]" action Jump(adjacent.label) #need to think how to change this when clicked
        textbutton "Return" action Return()

screen location_people(location):
    frame:
        has vbox
        text "People" color "#48ff00"
        for person in location.people:
            text "[person]"

screen location_inventory(location):
    frame:
        has vbox
        text "Inventory" color "#48ff00"
        for item in location.inventory:
            textbutton "[item]" action NullAction() #return a nullaction to test buttons

# TODO: clean-up the frames, and let the people decide what to do with the screen

# This screen toggle a selected screen to hide and show with a textbutton
# screen_name as a string, opcional parameter to the screen
screen toggle_dynamic_screen(screen_name, variable=None):
    default visible = False

    textbutton ("<" if visible else ">"):
        action ToggleScreenVariable('visible')

    showif visible:
        frame:
            use expression screen_name pass (variable)
            xsize 384 ysize 768
            at transform:
                on show:
                    ease 0.5 xanchor 0.0 xpos 16 ypos 500
                on hide:
                    xanchor 0.0 xpos 16
                    ease 0.5 xanchor 1.0 xpos 0


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"}

# 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.
    pass


code:
label layers_test2:
    "Testing layered clothes"

    "Looping"