init python:
    class Doll(DollMethods):
        def __init__(self, name, clothes, face, body):
            self.wardrobe = {}
            self.wardrobe_list = []
            self.blacklist = []
            self.outfits = []
            self.name = name
            self.clothes = clothes
            self.face = DollFace(self, face)
            self.body = DollBody(self, body)
            self.cum = DollCum(self)
            self.pose = None
            self.emote = Null()

            # Image properties
            self.zorder = 15
            self.layer = "screens"
            self.animation = None
            self.tag = get_character_tag(name)

            # Transform properties
            self.pos = (0, 0)
            self.zoom = 0.5
            self.xzoom = 1
            self.align = (0.5, 1.0)

        def rebuild(self):
            """Rebuild character image cache."""
            self.body.rebuild_image()
            self.face.rebuild_image()
            self.cum.rebuild_image()
            for o in self.wardrobe_list:
                o.rebuild_image()
                o.rebuild_icon()
            for o in self.outfits:
                o.rebuild_image()
            self.rebuild_image()

        def rebuild_image(self):
            self.cached = False

            if renpy.showing(get_character_tag(self.name), layer=self.layer):
                self.show()

        def show(self):
            if renpy.get_screen(("wardrobe", "animatedCG", "studio")):
                return

            base_transform = doll_transform(self.pos, self.zoom, self.xzoom)
            animation = self.animation

            at_list = [base_transform]

            if animation:
                at_list.append(animation)

            renpy.show(name=self.tag, at_list=at_list, layer=self.layer, what=self.get_image(), zorder=self.zorder)

        def hide(self):
            renpy.hide(name=self.tag, layer=self.layer)

        def build_image(self):
            # Add body, face, cum, clothes, masks
            masks = []
            sprites = [
                (self.body.get_image(), 0),
                (self.face.get_image(), 1),
                (self.cum.get_image(), self.cum.zorder_cum),
                (self.emote, 1000),
            ]

            for i in self.clothes.itervalues():
                clothing, _, is_worn = i

                if not clothing is None and is_worn:
                    zorder = clothing.zorder

                    sprites.extend([
                        (clothing.get_image(), zorder),
                        clothing.get_back(),
                        clothing.get_front(),
                        clothing.get_armfix(),
                        ])

                    if clothing.mask:
                        masks.append((clothing.mask, zorder-1))

            sprites.sort(key=itemgetter(1))
            masks.sort(key=itemgetter(1))

            back_sprites = [x for x in sprites if x[1] < 0]
            sprites = [x for x in sprites if x[1] >= 0]

            # Apply alpha mask
            for m in masks:
                mask, mask_zorder = m

                for i, s in enumerate(sprites):
                    sprite, sprite_zorder = s

                    if i < 1 or mask_zorder > sprite_zorder:
                        continue

                    c = tuple(x[0] for x in sprites[:i] if not isinstance(x[0], Null))
                    masked = AlphaMask(Fixed(*c, fit_first=True), mask)
                    sprites = sprites[i:]
                    sprites.insert(0, (masked, mask_zorder))
                    break

            sprites = back_sprites + sprites
            return tuple(x[0] for x in sprites if not isinstance(x[0], Null))

        def equip(self, obj, remove_old=True):
            """Takes DollCloth or DollOutfit object to equip."""
            if isinstance(obj, DollCloth):
                self.clothes[obj.type][0] = obj
                self.clothes[obj.type][2] = True

                if self.is_blacklisted(obj.type):
                    self.unequip(*self.get_blacklister(obj.type))

                if obj.blacklist:
                    self.unequip(*obj.blacklist)

                if self.pose:
                    obj.set_pose(self.pose)
            elif isinstance(obj, DollOutfit):
                if remove_old:
                    self.unequip("all")

                for i in obj.group:
                    self.clothes[i.type][0] = i.parent
                    self.clothes[i.type][0].set_color(i.color)
                    if self.pose:
                        i.parent.set_pose(self.pose)
            elif isinstance(obj, (list, tuple)):
                for cloth in obj:
                    self.clothes[cloth.type][0] = cloth
                    self.clothes[cloth.type][2] = True

                    if self.is_blacklisted(cloth.type):
                        self.unequip(*self.get_blacklister(cloth.type))

                    if cloth.blacklist:
                        self.unequip(*cloth.blacklist)

                    if self.pose:
                        cloth.set_pose(self.pose)

            self.body.rebuild_image()
            self.rebuild_image()
            self.rebuild_blacklist()
            update_chibi(self.name)

        def unequip(self, *args):
            """Takes argument(s) containing string cloth type(s) to unequip."""
            if "all" in args:
                for k, v in self.clothes.iteritems():
                    if not k in self.blacklist_unequip:
                        if self.pose and v[0]:
                            v[0].set_pose(None)
                        v[0], v[2] = None, True
            else:
                for arg in args:
                    if not arg in self.blacklist_unequip:
                        if self.pose and self.clothes[arg][0]:
                            self.clothes[arg][0].set_pose(None)
                        self.clothes[arg][0] = None

            self.body.rebuild_image()
            self.rebuild_image()
            self.rebuild_blacklist()
            update_chibi(self.name)

        def get_equipped(self, type):
            """Takes argument containing string cloth type. Returns equipped object for cloth type."""
            return self.clothes[type][0]

        def get_equipped_item(self, items):
            """Returns first equipped item from a list or None."""
            for i in items:
                if self.is_equipped_item(i):
                    return i
            return None

        def strip(self, *args):
            """Takes argument(s) containing string cloth type(s) to temporarily displace (hide)."""
            if "all" in args:
                for k, v in self.clothes.iteritems():
                    if not k.startswith(self.blacklist_toggles):
                        v[2] = False
            else:
                for arg in args:
                    if arg in self.multislots:
                        for k, v in self.clothes.iteritems():
                            if k.startswith(arg):
                                v[2] = False
                    else:
                        self.clothes[arg][2] = False
            self.body.rebuild_image()
            self.rebuild_image()
            update_chibi(self.name)

        def wear(self, *args):
            """Takes argument(s) containing string cloth type(s) to put on (unhide)."""
            if "all" in args:
                if self.is_worn("all"):
                    return

                for v in self.clothes.itervalues():
                    v[2] = True
            else:
                for arg in args:
                    if arg in self.multislots:
                        for k, v in self.clothes.iteritems():
                            if k.startswith(arg):
                                v[2] = True
                    else:
                        self.clothes[arg][2] = True
            self.body.rebuild_image()
            self.rebuild_image()
            update_chibi(self.name)

        def is_equipped(self, *args):
            """Takes argument containing string cloth type. Returns True if slot is occupied, False otherwise."""
            for arg in args:
                if arg in self.multislots:
                    return any(bool(v[0]) for k, v in self.clothes.iteritems() if k.startswith(arg))
                else:
                    if not self.clothes[arg][0]:
                        return False
            return True

        def is_any_equipped(self, *args):
            """Takes arguments containing string cloth types. Returns True if ANY of them is equipped, False otherwise."""
            if "clothes" in args:
                for k, v in self.clothes.iteritems():
                    if not k.startswith(self.blacklist_toggles):
                        if self.is_equipped(k):
                            return True
            else:
                for arg in args:
                    if self.is_equipped(arg):
                        return True
            return False

        def is_equipped_item(self, item):
            """Takes DollCloth object or list of objects. Returns True if item is equipped, False otherwise."""
            return self.get_equipped(item.type) == item

        def is_worn(self, *args):
            """Takes argument(s) containing string cloth type(s). Returns True if worn, False otherwise."""
            if "all" in args:
                for v in self.clothes.itervalues():
                    if not v[2]:
                        return False
            else:
                for arg in args:
                    if arg in self.multislots:
                        return any( (v[0] and v[2]) for k, v in self.clothes.iteritems() if k.startswith(arg))
                    else:
                        if not self.clothes[arg][0] or not self.clothes[arg][2]:
                            return False
            return True

        def is_any_worn(self, *args):
            """Takes arguments containing string cloth types. Returns True if ANY of them is worn, False otherwise."""
            if "clothes" in args:
                for k, v in self.clothes.iteritems():
                    if not k.startswith(self.blacklist_toggles):
                        if self.is_worn(k):
                            return True
            else:
                for arg in args:
                    if self.is_worn(arg):
                        return True
            return False

        def set_face(self, **kwargs):
            """Takes keyword argument(s) with the string name of expression file(s)."""
            if self.face.set_face(**kwargs):
                self.body.rebuild_image()

                # Rebuild lipstick
                lipstick = self.clothes.get("makeup4", [None, 1, True])[0]
                if isinstance(lipstick, DollLipstick):
                    lipstick.rebuild_image()
                self.rebuild_image()

        def get_face(self):
            """Returns a dictionary containing currently set facial expressions. Used in character studio."""
            return self.face.get_face()

        def set_body(self, **kwargs):
            """Takes keyword argument(s) with the string name of body part file(s)."""
            if self.body.set_body(**kwargs):
                self.rebuild_image()

        def set_body_hue(self, arg):
            """Takes integer between 0 - 359, rotates the character body colour by given amount."""
            self.body.hue = arg
            self.body.rebuild_image()
            self.rebuild_image()

        def set_body_zorder(self, **kwargs):
            """Takes keyword argument(s) with the name(s) of body part(s) and integer value(s)"""
            if self.body.set_zorder(**kwargs):
                self.rebuild_image()

        def set_cum(self, *args, **kwargs):
            """Takes keyword argument(s) containing string name(s) of cum layers to apply or None."""
            if self.cum.set_cum(*args, **kwargs):
                self.body.rebuild_image()
                self.rebuild_image()

        def set_pose(self, pose):
            if pose is None or renpy.loadable("characters/{}/poses/{}/loadable.webp".format(self.name, pose)):
                self.pose = pose
                self.face.set_pose(pose)
                self.body.set_pose(pose)
                self.cum.set_pose(pose)
                for v in self.clothes.itervalues():
                    if v[0]:
                        v[0].set_pose(pose)
                self.rebuild_image()
            else:
                raise Exception("'{}' pose doesn't exist for character named '{}'.".format(pose, self.name))

        def rebuild_blacklist(self):
            blacklist = []
            for v in self.clothes.itervalues():
                if v[0]:
                    blacklist.extend(v[0].blacklist)
            self.blacklist = list(set(blacklist))

        def is_blacklisted(self, type):
            """Takes string cloth type. Returns True if cloth type is blacklisted."""
            return True if type in self.blacklist else False

        def get_blacklister(self, type):
            """Takes string cloth type. Returns a list of clothing types that report incompatibility."""
            return [x[0].type for x in self.clothes.itervalues() if x[0] and type in x[0].blacklist]

        def create_outfit(self, temp=False):
            """Creates a copy of the current character clothes and stores it."""
            return DollOutfit([x[0] for x in self.clothes.itervalues() if x[0]], True, temp=temp)

        def import_outfit(self, path, fromfile=True):
            """Imports outfit from .png file or clipboard text."""
            # Grab data
            if fromfile:
                try:
                    imported = image_payload.decode(path)
                except:
                    if image_payload._file:
                        image_payload._file.close()
                    renpy.notify("Import failed: Corrupted file.")
                    return None
            else:
                imported = get_clipboard()

            # Evaluate data
            if imported:
                try:
                    imported = make_revertable(evaluate(imported))
                except:
                    renpy.notify("Import failed: Corrupted outfit data.")
                    renpy.block_rollback()
                    return None

                group = []

                for i, x in enumerate(imported):
                    if i == 0 and not x == self.name:
                        renpy.notify("Import failed: Wrong character.")
                        return None

                    for o in self.wardrobe_list:
                        if x[0] == o.id:
                            if not o.unlocked and not game.cheats:
                                renpy.notify("Import failed: You don't own these items. Buy them first.")
                                return None

                            x[0] = o.clone()
                            x[0].set_color(x[1])
                            group.append(x[0])

                if group:
                    renpy.notify("Import successful!")
                    return DollOutfit(group, True)
            renpy.notify("Import failed: Unknown error.")
            return None

        def get_schedule(self):
            """Returns a list of outfits available for current time of day and weather conditions."""
            schedule = []

            for o in self.outfits:
                if o.unlocked and o.schedule["day" if game.daytime else "night"]:
                    if game.weather == "overcast" and o.schedule["cloudy"]:
                        schedule.append(o)
                    elif game.weather in {"storm", "rain"} and o.schedule["rainy"]:
                        schedule.append(o)
                    elif game.weather in {"snow", "blizzard"} and o.schedule["snowy"]:
                        schedule.append(o)
                    elif game.weather in {"clear", "cloudy"} and not (o.schedule["cloudy"] or o.schedule["rainy"] or o.schedule["snowy"]):
                        schedule.append(o)
            return schedule

        def equip_random_outfit(self):
            """Equips random outfit based on Outfits Schedule."""
            schedule = self.get_schedule()

            if schedule:
                self.equip(renpy.random.choice(schedule))

        def set_emote(self, emote):

            if not emote and not isinstance(emote, Null):
                self.emote = Null()
                return

            if self.pose:
                path = "characters/{}/poses/{}/emote/{}.webp".format(self.name, self.pose, emote)
            else:
                path = "characters/{}/emote/{}.webp".format(self.name, emote)

            self.emote = DollDisplayable(Image(path))

