* Reduced memory footprint by excluding DollCloth and DollOutfit instances from directly participating in rollback unless reachable through renpy scope
285 lines
11 KiB
Plaintext
285 lines
11 KiB
Plaintext
init python:
|
|
class DollCloth(DollMethods, SlottedNoRollback):
|
|
layer_types = {
|
|
"mask": "-1",
|
|
"skin": 10,
|
|
"armfix": "+1",
|
|
"outline": None,
|
|
"extra": None,
|
|
"overlay": None,
|
|
}
|
|
|
|
layer_modifiers = {
|
|
"back": "-300",
|
|
"front": "+300",
|
|
"zorder": None,
|
|
}
|
|
|
|
def __init__(self, name, categories, type, id, color, zorder=None, unlocked=False, level=0, blacklist=[], modpath=None, parent=None):
|
|
self.name = name
|
|
self.categories = categories
|
|
self.type = type
|
|
self.id = id
|
|
self.color = [Color( (tuple(x) if isinstance(x, list) else x) ) for x in color] if color else None
|
|
self.unlocked = unlocked
|
|
self.level = level
|
|
self.blacklist = blacklist
|
|
self.modpath = "mods/" + posixpath.normpath(modpath) if modpath else ""
|
|
self.parent = parent
|
|
|
|
self.char = eval(name)
|
|
self.color_default = [x for x in self.color] if self.color else None
|
|
self.zorder = zorder or self.char.states[type][1]
|
|
self.seen = self.unlocked
|
|
self._hash = self.generate_hash()
|
|
|
|
# Add to character wardrobe and unordered list
|
|
if not parent:
|
|
self.char.wardrobe.setdefault(self.categories[0], {}).setdefault(self.categories[1], []).append(self)
|
|
self.char.wardrobe_list.append(self)
|
|
|
|
# Define new item slot type if doesn't exist
|
|
self.char.states.setdefault(self.type, [None, (self.zorder or 1), True])
|
|
|
|
def __repr__(self):
|
|
return f"DollCloth(name={self.name}, categories={self.categories}, type={self.type}, id={self.id}, color={self.color}, zorder={self.zorder}, unlocked={self.unlocked}, level={self.level}, blacklist={self.blacklist}, parent={self.parent}, modpath={self.modpath or None})"
|
|
|
|
def __hash__(self):
|
|
return self._hash
|
|
|
|
def __eq__(self, obj):
|
|
if not isinstance(obj, DollCloth):
|
|
return NotImplemented
|
|
return self._hash == obj._hash
|
|
|
|
def generate_hash(self):
|
|
salt = str( [self.name, self.char.pose, self.type, self.id, str(self.color), str(self.char.body._hash)] )
|
|
return hash(salt)
|
|
|
|
@functools.cache
|
|
def get_layers(self, hash, subpath=""):
|
|
path = posixpath.join(self.modpath, "characters", self.name, "poses", self.char.pose, subpath, "clothes", self.type, self.id)
|
|
|
|
extensions = self.extensions
|
|
types = self.layer_types
|
|
modifiers = self.layer_modifiers
|
|
|
|
layers = {}
|
|
for f in renpy.list_files():
|
|
fp, fn = os.path.split(f)
|
|
fn, ext = os.path.splitext(fn)
|
|
|
|
if not fp == path or not ext in extensions:
|
|
continue
|
|
|
|
# For user's sake, simplicty, and compatibility reasons,
|
|
# we sort the layers in the code instead.
|
|
#
|
|
# Each file name part represents the following:
|
|
# layertype_subtype*_zorder*_INT*
|
|
#
|
|
# Parts marked with * are optional and can be inserted out of order,
|
|
# with the exception of zorder, which requires an integer appendix.
|
|
#
|
|
# Example valid file name combinations:
|
|
#
|
|
# 0.webp
|
|
# 0_zorder_15.webp
|
|
# 0_front.webp
|
|
# outline.webp
|
|
# outline_back.webp
|
|
#
|
|
# If multiple files exist but with different extension,
|
|
# only the first file will be added to the dictionary.
|
|
|
|
ltype, *tails = fn.rsplit("_")
|
|
|
|
if not ltype.isdigit() and not ltype in types:
|
|
print("Invalid layer type for file: {}".format(f))
|
|
continue
|
|
|
|
zorder = z if (z := types.get(ltype)) is not None else self.zorder
|
|
|
|
if isinstance(zorder, str):
|
|
# Note: Layer uses relative zorder if it's passed as a string
|
|
zorder = self.zorder + int(zorder)
|
|
|
|
if tails:
|
|
lmodifier, *tails = tails
|
|
|
|
if not lmodifier in modifiers:
|
|
print("Invalid modifier for file: {}".format(f))
|
|
continue
|
|
|
|
zorder_mod = modifiers.get(lmodifier)
|
|
zorder = (zorder + int(zorder_mod)) if lmodifier != "zorder" else int(tails[-1])
|
|
layers.setdefault("_".join([ltype, lmodifier]), [f, zorder])
|
|
else:
|
|
layers.setdefault(ltype, [f, zorder])
|
|
|
|
return layers
|
|
|
|
@functools.cache
|
|
def build_image(self, hash, subpath="", matrix=None):
|
|
if matrix is None:
|
|
matrix = self.char.body.hue
|
|
|
|
processors = {
|
|
"skin": lambda file, _: Transform(file, matrixcolor=matrix),
|
|
"armfix": lambda file, _: Transform(file, matrixcolor=matrix),
|
|
"colored": lambda file, n: self.apply_color(file, int(n)),
|
|
"default": lambda file, _: Image(file),
|
|
}
|
|
|
|
layers = self.get_layers(hash, subpath)
|
|
|
|
sprites = []
|
|
for identifier, (file, zorder) in layers.items():
|
|
|
|
if ((n := identifier.rsplit("_", 1)[0]).isdigit()):
|
|
processor = processors["colored"]
|
|
else:
|
|
processor = processors.get(identifier, processors["default"])
|
|
|
|
processed_file = processor(file, n)
|
|
sprites.append((identifier, processed_file, zorder))
|
|
|
|
return sprites
|
|
|
|
@property
|
|
def image(self):
|
|
if not renpy.is_skipping() and self.is_stale():
|
|
hash = self._hash
|
|
|
|
sprites = self.build_image(hash)
|
|
sprites.sort(key=itemgetter(2))
|
|
sprites = [x[1] for x in sprites]
|
|
|
|
self._image = Fixed(*sprites, fit_first=True)
|
|
return self._image
|
|
|
|
@functools.cache
|
|
def build_icon(self, hash):
|
|
matrix = SaturationMatrix(0.0)
|
|
sprites = [i for i in self.build_image(hash, matrix=matrix) if not i[0] == "mask"]
|
|
|
|
try:
|
|
bounds = self.get_layers(hash).get("outline", [sprites[0][1]])[0]
|
|
except IndexError:
|
|
print(f"Missing textures:\n{self.__repr__()}")
|
|
return Text(f"TexErr\n{{color=#00ffff}}{{size=-6}}ID:{self.id}{{/size}}{{/color}}", color="#ff0000")
|
|
sprites.extend(self.char.body.build_image(self.char.body._hash, matrix=matrix))
|
|
sprites.sort(key=itemgetter(2))
|
|
|
|
wmax, hmax = self.sizes
|
|
wmin = hmin = 96
|
|
|
|
x, y, w, h = crop_whitespace(bounds)
|
|
xoffset, yoffset = w/2, h/2
|
|
|
|
w = h = max(w, h, wmin, hmin)
|
|
|
|
w = max(wmin, w + w/2)
|
|
h = max(hmin, h + h/2)
|
|
|
|
x = clamp( (x - w/2) + xoffset, 0, wmax)
|
|
y = clamp( (y - h/2) + yoffset, 0, hmax)
|
|
|
|
# Forbid exceeding the image height.
|
|
if y+h > hmax:
|
|
y = hmax-h
|
|
|
|
return Transform(Fixed(*[i[1] for i in sprites], fit_first=True), crop=(x, y, w, h))
|
|
|
|
@property
|
|
def icon(self):
|
|
return self.build_icon(self._hash)
|
|
|
|
def apply_color(self, img, n):
|
|
"""Takes image and int layer number. Used internally."""
|
|
try:
|
|
c = TintMatrix(self.color[n])
|
|
return Transform(img, matrixcolor=c)
|
|
except TypeError:
|
|
print(f"Item doesn't support colors but was supplied with a list; Try removing numbered files in its directory:\n{self.__repr__()}")
|
|
d = At(Frame(Text("TypeErr", color="#ffff00"), tile=True), blink_repeat)
|
|
return Transform(AlphaMask(d, img))
|
|
except IndexError:
|
|
print(f"Mismatched number of textures and colors; Try reducing number of supplied colours in item definition:\n{self.__repr__()}")
|
|
d = At(Frame(Text("IndxErr", color="#ff0000"), tile=True), blink_repeat)
|
|
return Transform(AlphaMask(d, img))
|
|
|
|
def set_color(self, n):
|
|
"""Takes int layer number for manual color picking or a list to replace the cloth color in its entirety."""
|
|
if isinstance(n, int):
|
|
col = self.color[n]
|
|
dcol = self.color_default[n]
|
|
|
|
cp.live_replace(col)
|
|
cp.start_replace(col)
|
|
cp.default_replace(dcol)
|
|
|
|
renpy.show_screen("colorpickerscreen", self)
|
|
|
|
while True:
|
|
try:
|
|
action, value = ui.interact()
|
|
except:
|
|
print(f"{ui.interact()}")
|
|
break
|
|
|
|
if action == "layer":
|
|
n = value
|
|
col = self.color[value]
|
|
dcol = self.color_default[value]
|
|
|
|
cp.live_replace(col)
|
|
cp.start_replace(col)
|
|
cp.default_replace(dcol)
|
|
elif action == "released":
|
|
self.color[n] = value
|
|
self.is_stale()
|
|
elif action == "replace":
|
|
self.color[n] = value
|
|
cp.live_replace(value)
|
|
self.is_stale()
|
|
elif action == "finish":
|
|
break
|
|
|
|
renpy.hide_screen("colorpickerscreen")
|
|
elif isinstance(n, list):
|
|
self.color = [Color( (tuple(x) if isinstance(x, list) else x) ) for x in n]
|
|
self.is_stale()
|
|
|
|
def reset_color(self, n=None):
|
|
"""Reset cloth color. Takes optional int layer number to reset only specific layer color."""
|
|
if n:
|
|
self.color[n] = self.color_default[n]
|
|
else:
|
|
self.color = [x for x in self.color_default]
|
|
self.is_stale()
|
|
|
|
def clone(self):
|
|
"""Creates a clone of this cloth object. Since it requires a parent object it should be used internally only to avoid object depth issue."""
|
|
return DollCloth(self.name, self.categories, self.type, self.id, [x for x in self.color] if self.color else None, self.zorder, self.unlocked, self.level, self.blacklist, self.modpath, self)
|
|
|
|
def is_modded(self):
|
|
"""Returns True if item comes from a mod."""
|
|
return bool(self.modpath)
|
|
|
|
def get_modname(self):
|
|
"""Return the name of the mod directory if exists."""
|
|
return self.modpath.split("/")[1] if self.is_modded() else None
|
|
|
|
def mark_as_seen(self):
|
|
self.seen = True
|
|
|
|
def is_multislot(self):
|
|
return self.type in self.multislots
|
|
|
|
def unlock(self):
|
|
self.unlocked = True
|
|
|
|
if self.parent:
|
|
self.parent.unlock()
|