From 4c98cbe669740604b98d2a48c322df3636b1e60f Mon Sep 17 00:00:00 2001 From: LoafyLemon Date: Tue, 11 Jul 2023 22:57:49 +0100 Subject: [PATCH] Wardrobe performance improvements and bug fixes * Implemented new DollThread method with thread-safe locking mechanism and pickling support for thread event queues * Added memoization for wardrobe elements * Added threading for various wardrobe-related methods * Added lazyloading (to avoid render stalls) * Added button generation for DollCloth and DollOutfit instances * Significantly reduced code repetition inside the wardrobe loop * Added new methods for the Doll, and improved others. * Fixed viewport adjustment values resetting on interaction * Fixed character chit-chats performance issues * Updated saves compatibility patch --- game/scripts/doll/clothes.rpy | 82 +++++- game/scripts/doll/common.rpy | 1 + game/scripts/doll/main.rpy | 25 +- game/scripts/doll/outfits.rpy | 109 +++++++- game/scripts/doll/threading.rpy | 88 ++++++- game/scripts/utility/updater.rpy | 10 +- game/scripts/wardrobe/functions.rpy | 22 +- game/scripts/wardrobe/reactions.rpy | 6 +- game/scripts/wardrobe/wardrobe.rpy | 371 ++++++++++------------------ 9 files changed, 457 insertions(+), 257 deletions(-) diff --git a/game/scripts/doll/clothes.rpy b/game/scripts/doll/clothes.rpy index dc45c23e..529696d7 100644 --- a/game/scripts/doll/clothes.rpy +++ b/game/scripts/doll/clothes.rpy @@ -15,6 +15,8 @@ init python: "zorder": None, } + _loading = Fixed(Text("Loading", align=(0.5, 0.5)), xysize=(96, 96)) + __slots__ = ("name", "categories", "type", "id", "color", "unlocked", "level", "blacklist", "modpath", "parent", "char", "color_default", "zorder", "seen", "_hash") def __init__(self, name, categories, type, id, color, zorder=None, unlocked=False, level=0, blacklist=[], modpath=None, parent=None): @@ -34,6 +36,7 @@ init python: self.zorder = zorder or self.char.states[type][1] self.seen = self.unlocked self._hash = self.generate_hash() + self._button = DefaultQueue() # Add to character wardrobe and unordered list if not parent: @@ -199,12 +202,89 @@ init python: if y+h > hmax: y = hmax-h - return Transform(Fixed(*[i[1] for i in sprites], fit_first=True), crop=(x, y, w, h)) + return Transform(Fixed(*[i[1] for i in sprites], fit_first=True), crop=(x, y, w, h), size=(96, 96), fit="contain", align=(0.5, 0.5)) @property def icon(self): return self.build_icon(self._hash) + def _build_button(self, _hash): + style = "wardrobe_button" + is_seen = self.seen + is_equipped = self.char.is_equipped_item(self) + is_inadequate = bool(get_character_progression(self.char.name) < self.level) + is_blacklisted = self.char.is_blacklisted(self.type) + is_blacklister = any(self.char.is_equipped(x) for x in self.blacklist) + is_modded = bool(self.modpath) + + warnings = [] + if is_blacklisted or is_blacklister: + blacklisted = [x for x in self.blacklist if self.char.is_equipped(x)] # Offender (List currently blacklisted clothing types by this item) + blacklister = self.char.get_blacklister(self.type) # Victim (List clothing types blacklisting this item ) + warnings.append("Incompatible with:{size=-4}\n" + "\n".join(set(blacklisted + blacklister)) + "{/size}") + + child = Frame(gui.format("interface/frames/{}/iconframe.webp"), 6, 6) + hbox = [] + overlay = [] + + action = [Return(["equip", self]), self.build_button] + unhovered = None + foreground = None + hover_foreground = "#ffffff80" + + if is_inadequate: + warnings.append("Character level too low") + + if is_modded: + warnings.append("Item belongs to a mod:\n{size=-4}{color=#35aae2}" + self.get_modname() + "{/color}{/size}") + hbox.append(Text("M", color="#00b200", style="wardrobe_button_text")) + + if is_blacklisted or is_blacklister: + hbox.append(Text("!", color="#b20000", style="wardrobe_button_text")) + + for i in self.char.wardrobe_list: + if i.unlocked and i.type == self.type: + action.append(i.build_button) + + if config.developer and self.level > 0: + hbox.append(Text(str(self.level), color="#00ffff", style="wardrobe_button_text")) + + if not is_seen: + unhovered = [Function(self.mark_as_seen), self.clear_button_cache, self.build_button] + overlay.append(Text("NEW", align=(1.0, 1.0), offset=(-6, -6), style="wardrobe_button_text")) + + if is_equipped: + overlay.append(Transform("interface/topbar/icon_check.webp", align=(1.0, 1.0), offset=(-6, -6), size=(24, 24))) + + if hbox: + overlay.append(HBox(*hbox, offset=(6, 6))) + + if overlay: + child = Fixed(child, *overlay, fit_first=True) + + if is_inadequate: + foreground = "#b2000040" + hover_foreground = "#CD5C5C40" + + return Button(child=child, focus_mask=None, xysize=(96, 96), background=self.icon, action=action, tooltip=("\n".join(warnings)), foreground=foreground, hover_foreground=hover_foreground, unhovered=unhovered, style=style) + + @functools.cache + def build_button(self, _=None): + + 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): + return self._button.get_with_default(self._loading) + + def clear_button_cache(self): + self.build_button.cache_clear() + def apply_color(self, img, n, maxsize): """Takes image and int layer number. Used internally.""" try: diff --git a/game/scripts/doll/common.rpy b/game/scripts/doll/common.rpy index 12e9cb56..763c9dbd 100644 --- a/game/scripts/doll/common.rpy +++ b/game/scripts/doll/common.rpy @@ -51,6 +51,7 @@ init -1 python: class DollMethods(SlottedObject): """Container class for commonly used methods and attributes""" + _loading = Text("Loading", align=(0.5, 0.5)) _image = Null() _image_cached = False blacklist_toggles = ("hair", "glasses", "pubes", "piercing", "makeup", "tattoo", "earrings") diff --git a/game/scripts/doll/main.rpy b/game/scripts/doll/main.rpy index 7c9380e9..e6f424b2 100644 --- a/game/scripts/doll/main.rpy +++ b/game/scripts/doll/main.rpy @@ -265,9 +265,9 @@ init python: """Takes argument containing string cloth type. Returns equipped object for cloth type.""" return self.states[slot][0] - def get_equipped_item(self, items): + def get_equipped_wardrobe_item(self, items, subcat): """Returns first equipped item from a list or None.""" - for i in items: + for i in items.get(subcat): if self.is_equipped_item(i): return i return None @@ -391,10 +391,17 @@ init python: def is_equipped_item(self, item): """Takes DollCloth object or list of objects. Returns True if item is equipped, False otherwise.""" - if item.is_multislot(): - return bool(next((k for k, v in self.states.items() if v[0] == item), False)) - return self.get_equipped(item.type) == item + if isinstance(item, DollCloth): + if item.is_multislot(): + return bool(next((k for k, v in self.states.items() if v[0] == item), False)) + + return self.get_equipped(item.type) == item + elif isinstance(item, DollOutfit): + compare_object = self.create_outfit(temp=True) + # current_item = next( (x for x in char_active.outfits if _outfit == x), None) + + return item == compare_object def is_worn(self, *args): """Takes argument(s) containing string cloth type(s). Returns True if worn, False otherwise.""" @@ -592,3 +599,11 @@ init python: self.emote = emote self.is_stale() + + def clear_outfit_button_cache(self): + + DollThread.stop_all() + + for i in self.outfits: + i._button.last_item = i._loading + i.clear_button_cache() diff --git a/game/scripts/doll/outfits.rpy b/game/scripts/doll/outfits.rpy index 0c679b71..9e12198c 100644 --- a/game/scripts/doll/outfits.rpy +++ b/game/scripts/doll/outfits.rpy @@ -1,8 +1,9 @@ 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") + __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] @@ -17,6 +18,7 @@ init python: self.hidden = hidden self.addons = addons self._hash = self.generate_hash() + self._button = DefaultQueue() if not self.temp: @@ -35,7 +37,7 @@ init python: return self._hash == obj._hash def generate_hash(self): - salt = str( [x._hash for x in self.group] ) + salt = str( [x._hash for x in self.group] ) + str(self.schedule) return hash(salt) def delete(self): @@ -83,6 +85,108 @@ init python: def image(self): return self.build_image(self._hash) + @functools.cache + def build_icon(self, hash): + sprite = self.build_image(self._hash) + + return Transform(sprite, crop=(220, 0, 680, 1200), size=(96, 168), fit="contain", align=(0.5, 1.0), yoffset=-6) + + @property + def icon(self): + return self.build_icon(self._hash) + + def _build_button(self, _hash, subcat): + global wardrobe_outfit_schedule + + style = "wardrobe_button" + # is_equipped = self.char.is_equipped_item(self) + is_modded = self.is_modded() + is_inadequate = subcat in ("save", "load", "schedule") and not wardrobe_check_equip_outfit(self) + has_schedule = any(self.schedule.values()) + + child = Frame(gui.format("interface/frames/{}/iconframe.webp"), 6, 6) + 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("M", color="#00b200", style="wardrobe_button_text")) + + action = None + alternate = None + unhovered = None + foreground = None + hover_foreground = "#ffffff80" + selected_foreground = None + + if is_inadequate: + foreground = "#b2000040" + hover_foreground = "#CD5C5C40" + selected_foreground = "#CD5C5C40" + + ## MOVE ACTIONS OUT OF THE FUNCTION, THEY FUCK THINGS UP. + ## One can manipulate the button actions using Button.action + + if subcat == "delete": + action = Return(["deloutfit", self]) + elif subcat == "load": + action = Return(["equip", self]) + elif subcat == "save": + action = Return(["addoutfit", self]) + # elif subcat == "import": # Imports are taken care of outside the class. + # action = Return(["import", self]) + elif subcat == "export": + action = Return(["export", self]) + elif subcat == "schedule": + if not has_schedule and not is_inadequate: + action = Return(["schedule", self]) + alternate = Return(["schedule", self]) + foreground = "#00000040" + hover_foreground = "#80808040" + selected_foreground = "#80808040" + elif has_schedule: + action = Return(["schedule", self]) + alternate = Return(["schedule", self]) + # elif is_inadequate: + # foreground = "#b2000040" + # hover_foreground = "#CD5C5C40" + # selected_foreground = "#CD5C5C40" + + if has_schedule: + for i in wardrobe_outfit_schedule: + if self.schedule[i]: + vbox.append(Transform(f"interface/wardrobe/icons/outfits/{i}.webp", size=(16, 16), offset=(6, 6))) + + # if is_equipped: + # hbox.append(Transform("interface/topbar/icon_check.webp", align=(1.0, 1.0), offset=(-6, -6), size=(24, 24))) + + if vbox: + hbox.append(VBox(*vbox)) + + if hbox: + child = Fixed(child, HBox(*hbox), fit_first=True) + + return Button(child=child, focus_mask=None, xysize=(96, 168), background=self.icon, action=action, alternate=alternate, tooltip=("\n".join(warnings)), foreground=foreground, hover_foreground=hover_foreground, selected_foreground=selected_foreground, unhovered=unhovered, style=style) + + @functools.cache + def build_button(self, subcat): + + def _func(self, hash, subcat): + result = self._build_button(self._hash, subcat) + self._button.put(result) + + thread = DollThread(target=_func, args=(self, self._hash, subcat)) + thread.start() + + @property + def button(self): + return self._button.get_with_default(self._loading) + + def clear_button_cache(self): + self.build_button.cache_clear() + def exists(self): return (self in self.char.outfits) @@ -178,3 +282,4 @@ init python: if self.has_type(arg): return True return False + diff --git a/game/scripts/doll/threading.rpy b/game/scripts/doll/threading.rpy index c060afa9..638bb88b 100644 --- a/game/scripts/doll/threading.rpy +++ b/game/scripts/doll/threading.rpy @@ -1,18 +1,92 @@ -init python: +init python early: import threading + import queue class DollThread(threading.Thread, NoRollback): - def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): + __lock = threading.RLock() + _count = 0 + _instances = _list() + _interval = 0.05 if renpy.android else 0.025 + + def __init__(self, interval=None, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): threading.Thread.__init__(self, group, target, name, args, kwargs, daemon=daemon) self._return = None + self.interval = interval or self._interval + self._delay = threading.Timer(self.interval, self._execute) + self._stop = threading.Event() + + def start(self): + DollThread._instances.append(self) + DollThread._count += 1 + super().start() def run(self): - if self._target is not None: - self._return = self._target(*self._args, **self._kwargs) + with DollThread.__lock: + self._delay.start() + self._delay.join() + self.stop() - renpy.restart_interaction() + def _execute(self): + while not self._stop.is_set(): + if self._target is not None: + self._return = self._target(*self._args, **self._kwargs) + renpy.restart_interaction() + break - def join(self, timeout=1): - threading.Thread.join(self, timeout=timeout) + def join(self, timeout=None): + super().join(timeout=timeout) return self._return + + def stop(self): + self._stop.set() + DollThread._instances.remove(self) + DollThread._count -= 1 + + @classmethod + def stop_all(cls): + with cls.__lock: + for thread in cls._instances: + thread.stop() + + # Allow threads to exit gracefully before forceful termination + for thread in cls._instances: + thread.join() + + class DefaultQueue(queue.Queue, NoRollback): + def __init__(self): + super().__init__() + self.last_item = None + + def put(self, item, block=True, timeout=None): + super().put(item, block, timeout) + self.last_item = item + + def get_with_default(self, default): + try: + return self.get(block=False) + except queue.Empty: + return self.last_item or default + + def __getstate__(self): + state = self.__dict__.copy() + del state['last_item'] + del state['mutex'] + del state['not_empty'] + del state['not_full'] + del state['all_tasks_done'] + return state + + def __setstate__(self, state): + self.__dict__.update(state) + self.last_item = None + self.mutex = threading.Lock() + self.not_empty = threading.Condition(self.mutex) + self.not_full = threading.Condition(self.mutex) + self.all_tasks_done = threading.Condition(self.mutex) + + def __reduce__(self): + return (DefaultQueue, ()) + + def __reduce_ex__(self, protocol): + return DefaultQueue, (), self.__getstate__(), None, iter([]) \ No newline at end of file diff --git a/game/scripts/utility/updater.rpy b/game/scripts/utility/updater.rpy index 1c8b915b..1823a108 100644 --- a/game/scripts/utility/updater.rpy +++ b/game/scripts/utility/updater.rpy @@ -101,11 +101,19 @@ init python: if current < 1.452: - # Fix makeup object types inside saved outfits + for i in states.dolls: doll = getattr(store, i) + for j in doll.wardrobe_list: + # Add new button handler for clothes + j._button = DefaultQueue() + for j in doll.outfits: + # Add new button handler for outfits + j._button = DefaultQueue() + + # Fix makeup object types inside saved outfits if j.has_type("makeup"): objects = [x.parent.clone() for x in j.group] diff --git a/game/scripts/wardrobe/functions.rpy b/game/scripts/wardrobe/functions.rpy index 206f52ae..92fd77ea 100644 --- a/game/scripts/wardrobe/functions.rpy +++ b/game/scripts/wardrobe/functions.rpy @@ -173,9 +173,27 @@ init -1 python: return def list_outfit_files(): - path = "{}/outfits/".format(config.gamedir) + + @functools.cache + def build_button(rp): + style = "wardrobe_button" + child = Fixed(Transform(rp, xsize=96, fit="contain", yalign=1.0, yoffset=-6), Frame(gui.format("interface/frames/{}/iconframe.webp"), 6, 6), xysize=(96, 168)) + action = Return(["import", rp]) + hover_foreground = "#ffffff80" + + return Button(child=child, action=action, hover_foreground=hover_foreground, style=style) + + path = f"{config.gamedir}/outfits/" if not os.path.exists(path): os.makedirs(path) - return [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and f.endswith(".png")] + files = [] + for f in os.listdir(path): + fp = os.path.join(path, f) + rp = os.path.relpath(fp, config.gamedir) + + if os.path.isfile(os.path.join(path, f)) and f.endswith(".png"): + files.append(build_button(rp)) + + return files diff --git a/game/scripts/wardrobe/reactions.rpy b/game/scripts/wardrobe/reactions.rpy index cb9f2bba..81ff28b8 100644 --- a/game/scripts/wardrobe/reactions.rpy +++ b/game/scripts/wardrobe/reactions.rpy @@ -68,10 +68,6 @@ init python: if wardrobe_chitchats: _skipping = True renpy.suspend_rollback(False) - renpy.hide_screen("wardrobe") - renpy.hide_screen("wardrobe_menuitem") - renpy.hide_screen("wardrobe_outfit_menuitem") - renpy.show("gui_fade", zorder=10, behind=get_character_tag(states.active_girl)) renpy.block_rollback() - renpy.call(get_character_response(states.active_girl, what), arg) + renpy.call_in_new_context(get_character_response(states.active_girl, what), arg) return diff --git a/game/scripts/wardrobe/wardrobe.rpy b/game/scripts/wardrobe/wardrobe.rpy index b58bda2a..ec311b6e 100644 --- a/game/scripts/wardrobe/wardrobe.rpy +++ b/game/scripts/wardrobe/wardrobe.rpy @@ -21,6 +21,9 @@ define wardrobe_categories = ("head", "piercings & tattoos", "upper body", "uppe define wardrobe_outfit_schedule = ("day", "night", "cloudy", "rainy", "snowy") init python: + + _lock = False + def preload_wardrobe_assets(): global _lock, _predicted _lock = True @@ -31,11 +34,62 @@ init python: _predicted = d _lock = False - _lock = False + def rebuild_wardrobe_icons(items, subcat): + for i in category_items.get(subcat, []): + i.build_button(subcat) - @functools.lru_cache(maxsize=100) - def create_wardrobe_icon(d, size=(96, 96), fit="contain", anchor=(0.5, 0.5), align=(0.5, 0.5), yoffset=0, crop=None): - return Transform(d, size=size, fit=fit, anchor=anchor, align=align, yoffset=yoffset, crop=crop) + def lock_wardrobe_icon(icon): + lock = bool(DollThread._count) + + return gray_tint(icon) if lock else icon + + def randomise_wardrobe_color(cloth): + + def _func(cloth): + if not cloth is None: + if wardrobe_randomise_color and cloth.color: + col_len = len(cloth.color) + col = [] + + for i in range(col_len): + if col_len == 1: + col.append(tetriadic_colors[0]) + elif col_len == 2: + col.append(double_colors[i-1]) + elif col_len == 3: + col.append(triadic_colors[i-1]) + else: + try: + col.append(tetriadic_colors[i-1]) + except: + col.append(col[-1].rotate_hue(0.33)) + + cloth.set_color(col) + + if wardrobe_global_color: + for outfit in char_active.outfits: + rebuild = False + + for i in outfit.group: + if not (i.id, i.type) == (cloth.id, cloth.type): + continue + + if len(cloth.color) != len(i.color): + print(f"Mismatched color lens:\n{cloth}\n{i}") + renpy.notify("Error!") + continue + + i.set_color(cloth.color) + i.is_stale() + rebuild = True + + if rebuild: + outfit.is_stale() + + rebuild_wardrobe_icons(category_items, current_subcategory) + + thread = DollThread(target=_func, args=(cloth,), interval=0.05) + thread.start() style loading_text: color "#ffffff" @@ -94,6 +148,10 @@ image loading_spinner: repeat label wardrobe: + $ renpy.call_in_new_context("_wardrobe") + return + +label _wardrobe: $ renpy.config.skipping = None $ _game_menu_screen = None $ _skipping = False @@ -137,10 +195,6 @@ screen wardrobe(xx, yy): use close_button fixed: - # TODO: Wardrobe doesn't work well with the gui animation - # if settings.get("animations"): - # at gui_animation - use wardrobe_menu(xx, yy) if current_category == "outfits": use wardrobe_outfit_menuitem(20, 50) @@ -165,10 +219,9 @@ label wardrobe_menu(): current_category = "head" category_items = OrderedDict(sorted(iter(list(wardrobe_subcategories.get(current_category, {}).items())), key=lambda x: wardrobe_subcategories_sorted.get(x[0], 0), reverse=True)) current_subcategory = list(category_items.keys())[0] if category_items else "" - menu_items = [x for x in category_items.get(current_subcategory, []) if x.unlocked==True] - icon_items = [create_wardrobe_icon(x.icon) for x in menu_items] - current_item = char_active.get_equipped_item(menu_items) + current_item = char_active.get_equipped_wardrobe_item(category_items, current_subcategory) last_track = renpy.music.get_playing() + rebuild_wardrobe_icons(category_items, current_subcategory) if wardrobe_music: play music "music/Spring_In_My_Step.ogg" fadein 1 if_changed @@ -176,7 +229,9 @@ label wardrobe_menu(): label .after_init: hide gui_fade - show screen wardrobe(662, 50) + + if not renpy.get_screen("wardrobe"): + show screen wardrobe(662, 50) $ renpy.hide(get_character_tag(states.active_girl)) $ renpy.config.skipping = None $ _game_menu_screen = None @@ -200,14 +255,9 @@ label wardrobe_menu(): $ current_subcategory = list(category_items.keys())[0] if category_items else "" if current_category == "outfits": - $ _outfit = char_active.create_outfit(temp=True) - $ menu_items = [x for x in reversed(category_items.get(current_subcategory, [])) if x.unlocked==True] - $ icon_items = [create_wardrobe_icon(x.image, crop=(220, 0, 680, 1200), size=(96, 168), fit="contain", anchor=(0.5, 1.0), align=(0.5, 1.0), yoffset=-6) for x in menu_items] - $ current_item = next( (x for x in char_active.outfits if _outfit == x), None) - else: - $ menu_items = [x for x in category_items.get(current_subcategory, []) if x.unlocked==True] - $ icon_items = [create_wardrobe_icon(x.icon) for x in menu_items] - $ current_item = char_active.get_equipped_item(menu_items) + $ char_active.clear_outfit_button_cache() + + $ current_item = char_active.get_equipped_wardrobe_item(category_items, current_subcategory) $ char_active.wear("all") if current_category in ("lower undergarment", "upper undergarment"): @@ -217,24 +267,18 @@ label wardrobe_menu(): else: $ wardrobe_react("category_fail", _choice[1]) + $ rebuild_wardrobe_icons(category_items, current_subcategory) + elif _choice[0] == "subcategory": if not current_subcategory == _choice[1]: $ current_subcategory = _choice[1] if current_category == "outfits": - $ _outfit = char_active.create_outfit(temp=True) - $ current_item = next( (x for x in char_active.outfits if _outfit == x), None) + $ char_active.clear_outfit_button_cache() - if current_subcategory == "import": - $ menu_items = list_outfit_files() - $ icon_items = [create_wardrobe_icon(f"outfits/{x}", size=(96, 168), fit="contain", anchor=(0.5, 1.0), align=(0.5, 1.0), yoffset=-6) for x in menu_items] - else: - $ menu_items = [x for x in reversed(category_items.get(current_subcategory)) if x.unlocked==True] - $ icon_items = [create_wardrobe_icon(x.image, crop=(220, 0, 680, 1200), size=(96, 168), fit="contain", anchor=(0.5, 1.0), align=(0.5, 1.0), yoffset=-6) for x in menu_items] - else: - $ menu_items = [x for x in category_items.get(current_subcategory) if x.unlocked==True] - $ icon_items = [create_wardrobe_icon(x.icon) for x in menu_items] - $ current_item = char_active.get_equipped_item(menu_items) + $ current_item = char_active.get_equipped_wardrobe_item(category_items, current_subcategory) + + $ rebuild_wardrobe_icons(category_items, current_subcategory) elif _choice[0] == "equip": ### CLOTHING ### @@ -243,11 +287,17 @@ label wardrobe_menu(): play sound "sounds/fail.ogg" $ renpy.notify("Hair cannot be removed.") else: + if char_active.is_equipped_item(_choice[1]): # UNEQUIP if wardrobe_check_unequip(_choice[1]): $ wardrobe_react("unequip", _choice[1]) $ char_active.unequip(_choice[1]) + + if current_item: + $ current_item.clear_button_cache() + $ current_item.build_button() + $ current_item = None else: $ wardrobe_react("unequip_fail", _choice[1]) @@ -260,8 +310,16 @@ label wardrobe_menu(): if not wardrobe_check_blacklist(_choice[1]): $ wardrobe_react("blacklist", _choice[1]) + $ _choice[1].mark_as_seen() $ char_active.equip(_choice[1]) + + if current_item: + $ current_item.clear_button_cache() + $ current_item.build_button() + $ current_item = _choice[1] + $ current_item.clear_button_cache() + $ current_item.build_button() if wardrobe_fallback_required(_choice[1]): # Has to be called regardless of player preference. @@ -297,6 +355,8 @@ label wardrobe_menu(): elif _choice[0] == "setcolor": python: current_item.set_color(_choice[1]) + current_item.clear_button_cache() + current_item.build_button() if wardrobe_global_color: for outfit in char_active.outfits: @@ -313,9 +373,6 @@ label wardrobe_menu(): if rebuild: outfit.is_stale() - menu_items = [x for x in category_items.get(current_subcategory) if x.unlocked==True] - icon_items = [create_wardrobe_icon(x.icon) for x in menu_items] - elif _choice[0] == "touch": if wardrobe_check_touch(_choice[1]): $ wardrobe_react("touch", _choice[1]) @@ -342,17 +399,17 @@ label wardrobe_menu(): _outfit.schedule = _old_schedule char_active.outfits[_index] = _outfit + _outfit.button(current_subcategory) renpy.notify("Overwritten.") else: renpy.notify("Save failed: Cancelled by user.") else: - char_active.create_outfit() + _outfit = char_active.create_outfit() + _outfit.build_button(current_subcategory) renpy.notify("Outfit Saved.") - menu_items = [x for x in reversed(category_items.get(current_subcategory)) if x.unlocked==True] - icon_items = [create_wardrobe_icon(x.image, crop=(220, 0, 680, 1200), size=(96, 168), fit="contain", anchor=(0.5, 1.0), align=(0.5, 1.0), yoffset=-6) for x in menu_items] - current_item = next( (x for x in char_active.outfits if _outfit == x), None) + current_item = char_active.get_equipped_wardrobe_item(category_items, current_subcategory) elif _choice[0] == "deloutfit": python: @@ -360,8 +417,6 @@ label wardrobe_menu(): if _confirmed: _choice[1].delete() - menu_items = [x for x in reversed(category_items.get(current_subcategory)) if x.unlocked==True] - icon_items = [create_wardrobe_icon(x.image, crop=(220, 0, 680, 1200), size=(96, 168), fit="contain", anchor=(0.5, 1.0), align=(0.5, 1.0), yoffset=-6) for x in menu_items] renpy.notify("Outfit Deleted.") elif _choice[0] == "export": @@ -405,7 +460,7 @@ label wardrobe_menu(): progress = get_character_progression(states.active_girl) if wardrobe_randomise_color: - + # Set once per interaction tetriadic_colors = [Color("%06x" % random.randint(0, 0xFFFFFF))] triadic_colors = [tetriadic_colors[0].rotate_hue(0.25)] double_colors = [tetriadic_colors[0], tetriadic_colors[0].rotate_hue(0.5)] @@ -453,51 +508,21 @@ label wardrobe_menu(): if valid_choices: cloth = random.choice(valid_choices) - if not cloth is None: - - if wardrobe_randomise_color and cloth.color: - col_len = len(cloth.color) - col = [] - - for i in range(col_len): - if col_len == 1: - col.append(tetriadic_colors[0]) - elif col_len == 2: - col.append(double_colors[i-1]) - elif col_len == 3: - col.append(triadic_colors[i-1]) - else: - try: - col.append(tetriadic_colors[i-1]) - except: - col.append(col[-1].rotate_hue(0.33)) - - cloth.set_color(col) - - if wardrobe_global_color: - for outfit in char_active.outfits: - rebuild = False - - for i in outfit.group: - if not (i.id, i.type) == (cloth.id, cloth.type): - continue - - if len(cloth.color) != len(i.color): - print(f"Mismatched color lens:\n{cloth}\n{i}") - renpy.notify("Error!") - continue - - i.set_color(cloth.color) - i.is_stale() - rebuild = True - - if rebuild: - outfit.is_stale() - + if cloth: + randomise_wardrobe_color(cloth) char_active.equip(cloth) else: char_active.unequip(k) + if current_item: + current_item.clear_button_cache() + current_item.build_button(current_subcategory) + + current_item = char_active.get_equipped_wardrobe_item(category_items, current_subcategory) + + if current_item: + current_item.clear_button_cache() + current_item.build_button(current_subcategory) else: #_choice == "Close": python: @@ -536,11 +561,16 @@ label wardrobe_menu(): renpy.hide_screen("wardrobe") char_active.wear("all") renpy.play('sounds/door2.ogg') + if wardrobe_music: renpy.music.play(last_track) + + DollThread.stop_all() + enable_game_menu() renpy.return_statement() + jump .after_init screen wardrobe_menu(xx, yy): @@ -629,8 +659,9 @@ screen wardrobe_menu(xx, yy): xysize (72, 72) align (0.0, 1.0) offset (10, -10) - background Transform("interface/wardrobe/icons/random.webp", size=(72,72)) + background lock_wardrobe_icon(Transform("interface/wardrobe/icons/random.webp", size=(72,72))) tooltip "Randomise Outfit" + sensitive (not DollThread._count) action Return("randomise") use dropdown_menu(name="Options", pos=(12, 56)): @@ -715,82 +746,22 @@ screen wardrobe_menuitem(xx, yy): tooltip subcategory action Return(["subcategory", subcategory]) - # Item icons - if not menu_items: - text "Nothing here yet" size 24 align (0.5, 0.6) - else: - vpgrid: - cols 5 - spacing 5 - pos (28, 192) - xysize (507, 308) - mousewheel True - scrollbars "vertical" + # # Item icons + # if not menu_items: + # text "Nothing here yet" size 24 align (0.5, 0.6) + # else: + vpgrid: + cols 5 + spacing 5 + pos (28, 192) + xysize (507, 308) + mousewheel True + scrollbars "vertical" - style_prefix gui.theme("wardrobe") + style_prefix gui.theme("wardrobe") - for item, icon in zip(menu_items, icon_items): - $ is_seen = item.seen - $ is_equipped = char_active.is_equipped_item(item) - $ is_inadequate = bool(get_character_progression(states.active_girl) < item.level) - $ is_blacklisted = char_active.is_blacklisted(item.type) - $ is_blacklister = any(char_active.is_equipped(x) for x in item.blacklist) - $ is_modded = bool(item.modpath) - $ is_multislot = item.is_multislot() - $ warnings = [] - - if is_blacklisted or is_blacklister: - $ blacklisted = [x for x in item.blacklist if char_active.is_equipped(x)] # Offender (List currently blacklisted clothing types by this item) - $ blacklister = char_active.get_blacklister(item.type) # Victim (List clothing types blacklisting this item ) - $ warnings.append("Incompatible with:{size=-4}\n" + "\n".join(set(blacklisted + blacklister)) + "{/size}") - - if is_inadequate: - $ warnings.append("Character level too low") - - if is_modded: - $ warnings.append("Item belongs to a mod:\n{size=-4}{color=#35aae2}" + item.get_modname() + "{/color}{/size}") - - # if is_multislot: - # $ slot = str(int(item.type[-1])+1) - # $ warnings.append("Occupies " + item.type[:-1] + " slot number " + slot) - - button: - focus_mask None - xysize icon_size - background icon - action Return(["equip", item]) - tooltip ("\n".join(warnings)) - if is_inadequate: - foreground "#b2000040" - hover_foreground "#CD5C5C40" - if not is_seen: - unhovered Function(item.mark_as_seen) - - add icon_frame - - hbox: - offset (5, 5) - - if is_modded: - text "M" color "#00b200" - - if is_blacklisted or is_blacklister: - text "!" color "#b20000" - - if config.developer: - text "\nReq. {}".format(item.level) size 10 color "#00ffff" outlines [(1, "#000000", 1, 1)] - - # Bottom-Right - if is_equipped: - add "interface/topbar/icon_check.webp" anchor (1.0, 1.0) align (1.0, 1.0) offset (-5, -5) zoom 0.5 - - # Bottom-Right - if not is_seen: - text "NEW" style "wardrobe_item_caption" anchor (1.0, 1.0) align (1.0, 1.0) offset (-5, -5) - - # # Bottom-Left - # if is_multislot: - # text "[slot]" style "wardrobe_item_caption" anchor (0.0, 1.0) align (0.0, 1.0) offset (5, -5) + for item in category_items.get(current_subcategory, []): + add item.button screen wardrobe_outfit_menuitem(xx, yy): tag wardrobe_menuitem @@ -856,81 +827,13 @@ screen wardrobe_outfit_menuitem(xx, yy): text_align (0.5, 0.5) action Return(["addoutfit", None]) - for item, icon in zip(menu_items, icon_items): - if current_subcategory == "import": - $ is_modded = False - $ is_equipped = False - else: - $ is_modded = item.is_modded() - $ is_equipped = bool(current_item == item) - $ is_inadequate = (current_subcategory in {"save", "load", "schedule"} and not wardrobe_check_equip_outfit(item)) - $ has_schedule = any(item.schedule.values()) + if current_subcategory == "import": + for item in list_outfit_files(): + add item + else: - $ warnings = [] - - if is_modded: - $ warnings.append("Outfit contains items from these mods:\n{size=-4}{color=#35aae2}"+ "\n".join(item.get_modname()) + "{/color}{/size}") - - $ alternate = None - if current_subcategory == "delete": - $ action = Return(["deloutfit", item]) - elif current_subcategory == "load": - $ action = Return(["equip", item]) - elif current_subcategory == "save": - $ action = Return(["addoutfit", item]) - elif current_subcategory == "import": - $ action = Return(["import", item]) - elif current_subcategory == "export": - $ action = Return(["export", item]) - elif current_subcategory == "schedule": - - if is_inadequate: - $ action = NullAction() - $ alternate = None - else: - $ action = Return(["schedule", item]) - $ alternate = Return(["schedule", item]) - - $ warnings = "\n".join(warnings) - - button: - focus_mask None - xysize icon_size - background icon - tooltip warnings - action action - alternate alternate - if current_subcategory == "schedule" and not has_schedule: - foreground "#00000040" - hover_foreground "#80808040" - selected_foreground "#80808040" - elif is_inadequate: - foreground "#b2000040" - hover_foreground "#CD5C5C40" - selected_foreground "#CD5C5C40" - - add icon_frame - - hbox: - offset (5, 5) - - if is_modded: - text "M" color "#00b200" - - if not current_subcategory in {"import", "export"} and get_character_scheduling(states.active_girl): - vbox: - pos (6, 6) - spacing 1 - for i in wardrobe_outfit_schedule: - if item.schedule[i]: - add Transform("interface/wardrobe/icons/outfits/{}.webp".format(i), size=(16, 16)) - - if is_equipped: - add "interface/topbar/icon_check.webp": - anchor (1.0, 1.0) - align (1.0, 1.0) - offset (-5, -5) - zoom 0.5 + for item in reversed(category_items.get(current_subcategory, [])): + add item.button screen wardrobe_schedule_menuitem(item): tag dropdown @@ -958,7 +861,7 @@ screen wardrobe_schedule_menuitem(item): textbutton i: style gui.theme("dropdown") tooltip caption - action ToggleDict(item.schedule, i, True, False) + action [ToggleDict(item.schedule, i, True, False), item.clear_button_cache, Function(item.build_button, current_subcategory)] style wardrobe_window is empty @@ -970,7 +873,7 @@ style wardrobe_button is empty: style wardrobe_button_text: color "#fff" size 20 - outlines [ (1, "#000", 0, 0) ] + outlines [ (2, "#000", 0, 0) ] style wardrobe_item_caption: color "#fff"