init python: class DollOutfit(DollMethods): default_schedule = {"day": False, "night": False, "cloudy": False, "rainy": False, "snowy": False} _loading = Fixed(Text("Loading", align=(0.5, 0.5)), xysize=(96, 168)) __slots__ = ("group", "name", "desc", "price", "char", "unlocked", "schedule", "temp", "hidden", "addons", "_hash", "_button") def __init__(self, group, unlocked=False, name="", desc="", price=0, temp=False, schedule={}, hidden=False, addons=[]): self.group = [x.clone() if not x.parent else x for x in group] self.group.sort() self.name = name self.desc = desc self.price = price self.char = self.group[0].char self.unlocked = unlocked self.schedule = dict(list(self.default_schedule.items()) + list(schedule.items())) self.temp = temp self.hidden = hidden self.addons = addons self._hash = self.generate_hash() self._button = DefaultQueue() self.imported = False if not self.temp: if unlocked: self.unlock() if not self.hidden and not self in self.char.outfits: self.char.outfits.append(self) def __hash__(self): return self._hash def __eq__(self, obj): if not isinstance(obj, DollOutfit): return NotImplemented return self._hash == obj._hash def generate_hash(self): salt = str( [x._hash for x in self.group] ) + str(self.schedule) return hashstring(salt) def delete(self): if self in self.char.outfits: self.char.outfits.remove(self) if self.imported: try: os.remove(os.path.join(config.gamedir, self.imported)) except: print(f"Warning! Failed to delete imported outfit file - {self.imported}") @functools.cache def build_image(self, hash): if (d := self.get_disk_cache(hash)): return d from itertools import chain matrix = SaturationMatrix(0.0) sprites = list(chain.from_iterable( (self.char.body.build_image(self.char.body._hash, matrix=matrix), *(x.build_image(x._hash, matrix=matrix) for x in self.group)) )) masks = [sprites.pop(sprites.index(x)) for x in sprites if x[0] == "mask"] sprites.sort(key=itemgetter(2)) masks.sort(key=itemgetter(2)) back_sprites = [x[1] for x in sprites if x[2] < 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 masked = AlphaMask(Fixed(*(x[1] for x in sprites[:i]), fit_first=True), mask) sprites = sprites[i:] sprites.insert(0, (None, masked, mask_zorder)) break sprites = back_sprites + [x[1] for x in sprites] d = Fixed(*sprites, fit_first=True) return self.create_disk_cache(d, hash) @property def image(self): return self.build_image(self._hash) @functools.cache def build_icon(self, hash): sprite = self.build_image(self._hash) return Transform(AlphaMask(Transform(sprite, crop=(250, 0, 680, 1200), fit="scale-down", ysize=168), Transform("wheelmenu_frame_opaque_vertical", xysize=(96, 168)), yalign=1.0, fit_first=True)) # 730x1020 @property def icon(self): return self.build_icon(self._hash) def _build_button(self, _hash): style = "wardrobe_item_rectangular_button" is_equipped = self.char.is_equipped_item(self) is_modded = self.is_modded() # is_inadequate = wardrobe.wardrobe_check_equip_outfit(self) has_schedule = any(self.schedule.values()) children = [self.icon] warnings = [] hbox = [] vbox = [] overlay = [] if is_modded: warnings.append("Outfit contains items from these mods:\n{size=-4}{color=#35aae2}"+ "\n".join(self.get_modname()) + "{/color}{/size}") hbox.append(Text("🛠️", align=(0.5, 0.5), style="wardrobe_icon_text")) if self.imported: warnings.append("This outfit has an associated outfit file in /outfits/ directory.") hbox.append(Text("📦", align=(0.5, 0.5), style="wardrobe_icon_text")) if renpy.android: action = [Function(wardrobe.wheelmenu, self, "outfits"), self.build_button] unhovered = None alternate = None else: action = [Function(wardrobe.equip, self), self.build_button] unhovered = None alternate = [Function(wardrobe.wheelmenu, self, "outfits"), self.build_button] if has_schedule: for i, emoji in wardrobe.outfit_schedule.items(): # NONLOCAL if self.schedule[i]: vbox.append(Text(emoji, align=(0.5, 0.5), offset=(3, 6), style="wardrobe_icon_text")) if vbox: children.append(VBox(*vbox, ypos=34)) if hbox: children.append(HBox(*hbox)) return Button(child=Fixed(*children, xysize=(96, 168)), focus_mask=None, xysize=(96, 168), action=action, tooltip=("\n".join(warnings)), unhovered=unhovered, style=style, selected=is_equipped, alternate=alternate) @functools.cache def build_button(self): def _func(self, hash): result = self._build_button(self._hash) self._button.put(result) thread = DollThread(target=_func, args=(self, self._hash)) thread.start() @property def button(self): if settings.get("multithreading"): return self._button.get_with_default(self._loading) else: global current_subcategory return self._build_button(self._hash) def clear_button_cache(self): self.build_button.cache_clear() def exists(self): return (self in self.char.outfits) def export_data(self, filename, tofile=True): """Exports outfit to .png file or clipboard text.""" # Export data consists of the following: # [0] - Character name # [1] - [Item ID, Colour List] exported = [self.group[0].name] for i in self.group: if i.color: color = [j.hexcode for j in i.color] exported.append([i.id, color]) # Encode data if tofile: fp_input = os.path.join("game", "outfits").replace("\\", "/") # Ren'py Paths are UNIX based fn_input = f"{self._hash}_temp.png" fn_output = f"{self._hash}.png" fpt_output = os.path.join(fp_input, fn_input).replace("\\", "/") # Ren'py Paths are UNIX based osfp = os.path.join(config.basedir, fpt_output) # Python paths are OS based if not os.path.exists(fp_input): os.makedirs(fp_input) width, height = self.sizes d = self.build_image(self._hash) # d = Transform(d, crop=(220, 0, 680, 1200), size=(96, 168), fit="contain", align=(0.5, 1.0), yoffset=-6) d = Fixed( d, Text(states.active_girl, align=(0.5, 0.995)), Text(f"Ver. {config.version}", size=24, align=(0.99, 0.99)) ) try: renpy.render_to_file(d, fpt_output, width=width, height=height, resize=True) except Exception as e: print(f"Warning! Failed to write to path: {fp}/{fn_input} - {e}") image_payload.inject(fn_input, fn_output, str(exported)) os.remove(osfp) # Remove temp file else: set_clipboard(exported) renpy.notify("Export successful!") def unlock(self): """Unlocks outfit and respective clothing objects from which they were cloned.""" self.unlocked = True for i in self.group: i.unlock() for i in self.addons: i.unlock() def save(self): """Overwrites this outfit with clothes currently equipped by the character.""" self.group = [x[0].clone() for x in self.char.states.values() if x[0]] return def is_modded(self): """Returns True if one of the group items comes from a mod.""" for i in self.group: if i.is_modded(): return True return False def get_modname(self): """Returns a list of mods contained within the outfit group.""" return list(set([i.get_modname() for i in self.group if i.is_modded()])) def get_schedule(self): """Returns a dictionary with the current schedule.""" return self.schedule def set_schedule(self, **kwargs): for k, v in kwargs.items(): self.schedule[k] = v def has_type(self, *args): """Takes argument(s) containing string cloth type(s). Returns True if worn, False otherwise.""" types = set(x.type for x in self.group) for arg in args: if arg in self.multislots: if not any(x.startswith(arg) for x in types): return False else: if not arg in types: return False return True def has_any_type(self, *args): """Takes arguments containing string cloth types. Returns True if ANY of them is worn, False otherwise.""" if "clothes" in args: for k in self.char.states.keys(): if not k.startswith(self.blacklist_toggles): if self.has_type(k): return True else: for arg in args: if self.has_type(arg): return True return False