WTS/game/scripts/doll/chibi.rpy

386 lines
14 KiB
Plaintext
Raw Normal View History

2022-05-16 23:48:22 +00:00
init 5 python:
# def pairwise(iterable):
# a = iter(iterable)
# return zip(a, a)
class DollChibi(renpy.Displayable):
layers_additional = {
"back": -100,
"front": 100,
"skin": 0.1,
"extra": 1,
}
extensions = [ ".jpg", ".jpeg", ".png", ".webp" ]
2022-05-16 23:48:22 +00:00
def __init__(self, name, doll, pose="stand", layer="screens", zorder=12, zoom=0.28, *args, **properties):
super(DollChibi, self).__init__(**properties)
self.name = name
self.doll = doll
self.pose = pose
self.idle = "stand"
2022-05-16 23:48:22 +00:00
self.layer = layer
self.zorder = zorder
self.zoom = zoom
self.pos = (0,0)
self.xzoom = 1
# Animation
self.anim_speed = 1.0
self.anim_fps = 8.0
self.anim_trans = None
self.anim_interval = None
self.anim_interval_total = None
self.anim_path = None
self.anim_constructor()
# ATL
self.atl_time = 0
self.atl_time_total = 0
self.atl_partial = None
self.atl_looping = False
self.atl_pause = False
self.atl_at_list = []
2022-05-16 23:48:22 +00:00
def anim_constructor(self):
"""This function is responsible for creating an animation out of raw image files."""
doll = self.doll
2022-05-16 23:48:22 +00:00
path = f"characters/{self.name}/chibi/{self.pose}"
files = [f for f in renpy.list_files() if f.startswith(path)]
2022-05-16 23:48:22 +00:00
images = []
groups = {}
2022-05-16 23:48:22 +00:00
# Construct Animation
for fn in files:
root = os.path.dirname(fn)
name, ext = os.path.splitext(os.path.basename(fn))
frame = name.split("_")[0]
2022-05-16 23:48:22 +00:00
if not ext.lower() in self.extensions:
2022-05-16 23:48:22 +00:00
continue
d = renpy.displayable(fn)
if name.isdigit() and root.endswith(path):
# Create frame group if filename is a frame number and not a cloth
groups.setdefault(name, {}).setdefault(0, [d]) # Body layer is zorder zero
continue
# Get frame id, clothing type, and clothing id
ctype, cid = root.rsplit("/")[-2::]
if not frame in groups:
# Frame group is missing, skip construction
continue
# Check if clothing type slot is occupied and currently visible
if (not doll.is_equipped(ctype) or
not doll.is_worn(ctype)):
continue
equipped = doll.get_equipped(ctype)
zorder = equipped.zorder
# Check if the supplied file is a special layer,
# in which case we need to treat it differently
for layer, modifier, in self.layers_additional.items():
suffix = f"_{layer}"
if name.endswith(suffix):
zorder += modifier
cid = cid.removesuffix(suffix)
break
# Check if clothing type matches
if not equipped.type == ctype:
continue
# Check if clothing id matches, if not, add fallback image instead
if not equipped.id == cid:
fpath = f"{path}/{ctype}/fallback/0"
for i in self.extensions:
f = fpath + i
if renpy.loadable(f):
d = renpy.displayable(f)
break
groups[frame].setdefault(zorder, []).append(d)
# Iterate through frame groups and nested zorder groups
for fgroup in groups.values():
igroup = []
# Sort zorder groups
for zgroup in dict(sorted(fgroup.items())).values():
# Unpack zorder groups into displayables
igroup.append(Fixed(*zgroup, fit_first=True))
images.append(Fixed(*igroup, fit_first=True))
2022-05-16 23:48:22 +00:00
if not images:
raise IOError(f"Animation Constructor could not find any images inside directory:\n\n\"{path}\"")
2022-05-16 23:48:22 +00:00
# For singular images it's optimal to halt the animation,
# instead of replaying the same frame over and over again.
frames = len(images)
if frames > 1:
interval = self.anim_speed / self.anim_fps
else:
interval = (365.25 * 86400.0) # About one year
self.anim_path = path
self.anim_frames = frames
self.anim_interval = interval
self.anim_interval_total = (frames * interval)
self.anim_images = images
self.anim_prev_images = [ images[-1] ] + images[:-1]
def render(self, width, height, st, at):
# Animation Renderer
t = st % self.anim_interval_total
trans = self.anim_trans
interval = self.anim_interval
# Trigger event after animation time elapses
if st > self.atl_time_total:
renpy.timeout(0)
for image, prev in zip(self.anim_images, self.anim_prev_images):
if t < interval:
if not renpy.game.less_updates:
renpy.redraw(self, interval - t)
if trans and st >= interval:
image = trans(old_widget=prev, new_widget=image)
im = renpy.render(image, width, height, t, at)
width, height = im.get_size()
rv = renpy.Render(width, height)
rv.blit(im, (0, 0))
return rv
else:
t = t - interval
def event(self, ev, x, y, st):
# Determine pose change if show time exceeds animation time.
# Looping animations ignore this check because they are rebuilt every loop.
if renpy.in_rollback():
# We don't want to use 'raise renpy.IgnoreEvent' because it blocks events in other displayables, including interfaces
return
elif renpy.is_skipping():
self.stop()
return
elif ((not self.atl_looping and st > self.atl_time_total)
or (ev.type == pygame.MOUSEBUTTONUP and ev.button == 1)
or (ev.type == pygame.KEYDOWN and ev.key in [pygame.K_RETURN, pygame.K_SPACE, pygame.K_KP_ENTER, pygame.K_SELECT])):
self.stop()
# def per_interact(self):
# # Handle interrupt events
# if (renpy.is_skipping() or self.atl_pause) and not self.pose == self.idle:
# self.stop()
2022-05-16 23:48:22 +00:00
def set_pose(self, pose):
if self.pose == pose:
return
self.pose = pose
self.anim_constructor()
def set_idle(self, pose):
if self.idle == pose:
return
self.idle = pose
self.anim_constructor()
def stop(self):
# Freezes the animation
path, _, _, _ = self.atl_partial.args
pos = path[-1]
xzoom = 1 if (path[-1] > path[-2]) else -1
zpos = self.zorder + (1.0 / (pos[1] / config.screen_height))
self.pos = pos
self.xzoom = xzoom
self.atl_time = 0
self.atl_time_total = 0
self.set_pose(self.idle)
transform = Transform(pos=pos, zoom=self.zoom, xzoom=xzoom, anchor=(0.5, 1.0))
self.show(transform, self.atl_at_list, self.layer, zpos)
def move(self, path, speed=1.0, pause=True, loop=False, warper="linear", at_list=[], pose="walk", repeat=None, wrap=True, reverse=True):
2022-05-16 23:48:22 +00:00
"""Makes chibi move"""
self.atl_looping = loop
self.atl_pause = pause
self.atl_at_list = at_list
2022-05-16 23:48:22 +00:00
if isinstance(path, tuple):
path = [path]
# If 'A' position is not supplied for A -> B movement, use last known position instead.
if len(path) < 2:
path = [self.pos] + path
# If reverse and loop/repeat is True, and pathing has more than two entries,
# use complex looping by joining the reversed paths
elif reverse and (loop or repeat):
# Reverses the list, strips last entry from the reverse list, and finally joins them.
path += path[1::-1]
if repeat:
path *= repeat
if wrap:
# Wrap pathing back to starting position
path.append(path[0])
2022-05-16 23:48:22 +00:00
self.set_pose(pose)
# Note: Warper names and their count can change over time,
# so it's easier to just evaluate the input.
# List of available warpers:
# https://www.renpy.org/doc/html/atl.html?#warpers
warper = eval(f"_warper.{warper}")
2022-05-16 23:48:22 +00:00
distances = []
times = []
if loop:
# Append first position as last to create a looped path.
path.append(path[0])
# Calculate distances and timings using euclidean distance algorithm.
for xy1, xy2 in zip(path, path[1:]):
x1, y1 = xy1
x2, y2 = xy2
distance = math.hypot(x2 - x1, y2 - y1)
time = distance / (100.0 * speed)
distances.append(distance)
times.append(time)
# Calculate total ATL time required to reach the destination
total_time = sum(times)
self.atl_time_total = total_time
# Recalculate animation intervals when necessary, including speed factors.
frames = self.anim_frames
if frames > 1:
interval = (self.anim_speed / self.anim_fps) / speed
interval_total = (frames * interval)
self.anim_interval = interval
self.anim_interval_total = interval_total
# renpy.partial allows us to pass arguments into a transform function.
partial = renpy.partial(self.move_atl, path, times, loop, warper)
self.atl_partial = partial
transform = Transform(function=partial, anchor=(0.5, 1.0))
self.show(transform, at_list, self.layer, self.zorder)
2022-05-16 23:48:22 +00:00
if pause:
if loop:
renpy.pause(None)
else:
renpy.pause(total_time)
2022-05-16 23:48:22 +00:00
return (distances, times)
def move_atl(self, path, times, loop, warper, trans, st, at):
"""Animations are time based, so each segment will happen at a specific frame time."""
if self.atl_time_total == 0:
# Stops updating the animation
return None
2022-05-16 23:48:22 +00:00
if loop:
timer = st % self.atl_time_total
else:
timer = st
if timer > self.atl_time_total:
return None
internal_time = 0
current_segment = 0
# TODO: This loop feels unnecessary, need to find a better way.
for i, t in enumerate(times):
if (internal_time + t) > timer:
current_segment = i
break
internal_time += t
segment_time = (timer - internal_time) / times[current_segment]
next_segment = current_segment + 1
# Adjust XY position
trans.pos = renpy.atl.interpolate(warper(segment_time), path[current_segment], path[next_segment], renpy.atl.PROPERTIES["pos"])
self.pos = trans.pos
# Adjust X zoom based on target X position
# 1 = Facing Right, -1 = Facing Left
trans.xzoom = -1 if (path[current_segment][0] > path[next_segment][0]) else 1
self.xzoom = trans.xzoom
# Adjust zoom
trans.zoom = self.zoom
# Adjust Z position based on Y axis
# TODO: Add room support with bottom, middle, and top vanishing points.
# room_scale = 0.5
# zpos1 = ((path[current_segment][1] / 600.0) * 1000.0) * room_scale
# zpos2 = ((path[next_segment][1] / 600.0) * 1000.0) * room_scale
# trans.zpos = renpy.atl.interpolate(warper(segment_time), zpos1, zpos2, renpy.atl.PROPERTIES["zpos"])
# self.zpos = trans.zpos
# TODO: Using zorders is suboptimal and expensive, using 3D staging would be preferable.
zpos = self.zorder + (1.0 / (self.pos[1] / config.screen_height))
2022-05-16 23:48:22 +00:00
renpy.change_zorder(self.layer, self.name, zpos)
return 0
def show(self, transform, at_list, layer, zorder):
2022-05-16 23:48:22 +00:00
# The safest way to restart the transform is to rebuild it.
# Other methods proved to be too finicky...
image = At(self, transform) # IMPORTANT: Enable perspective and gl_depth for 3D staging
if not renpy.is_init_phase():
renpy.show(name=self.name, what=image, at_list=at_list, layer=layer, zorder=zorder)
2022-05-16 23:48:22 +00:00
init offset = 5
default hooch_chibi = DollChibi(name="hooch", doll=hooch)
default cho_chibi_new = DollChibi(name="cho", doll=cho)
label chibitest:
"Rollback block"
$ renpy.block_rollback()
"repeat 3"
$ cho_chibi_new.move(path=[(500, 421), (650, 521), (800, 421)], repeat=3)
"loop"
$ cho_chibi_new.move(path=[(500, 421), (650, 521), (800, 421)], loop=True)
"End"
jump main_room_menu