240 lines
8.4 KiB
Plaintext
240 lines
8.4 KiB
Plaintext
init 5 python:
|
|
# def pairwise(iterable):
|
|
# a = iter(iterable)
|
|
# return zip(a, a)
|
|
|
|
class DollChibi(renpy.Displayable):
|
|
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.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
|
|
|
|
def anim_constructor(self):
|
|
"""This function is responsible for creating an animation out of raw image files."""
|
|
|
|
path = "characters/{}/chibi/{}/".format(self.name, self.pose)
|
|
images = []
|
|
|
|
# Base model
|
|
for fn in renpy.list_files():
|
|
if not fn.startswith(path):
|
|
continue
|
|
|
|
basename = os.path.basename(fn)
|
|
base, ext = os.path.splitext(basename)
|
|
|
|
if not ext.lower() in [ ".jpg", ".jpeg", ".png", ".webp" ]:
|
|
continue
|
|
|
|
images.append(renpy.displayable(fn))
|
|
|
|
if not images:
|
|
raise IndexError("Animation Constructor could not find any images inside directory:\n\n\"{}\"".format(path))
|
|
|
|
# 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 st > self.atl_time_total:
|
|
self.set_pose("stand")
|
|
|
|
def set_pose(self, pose):
|
|
if self.pose == pose:
|
|
return
|
|
|
|
self.pose = pose
|
|
self.anim_constructor()
|
|
|
|
def move(self, path, speed=1.0, pause=False, loop=False, warper="linear", at_list=[], pose="walk"):
|
|
"""Makes chibi move"""
|
|
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
|
|
|
|
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("_warper.{}".format(warper))
|
|
|
|
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
|
|
|
|
self.restart_atl()
|
|
|
|
if pause:
|
|
renpy.pause(total_time)
|
|
|
|
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 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 + self.pos[1] / config.screen_height
|
|
renpy.change_zorder(self.layer, self.name, zpos)
|
|
return 0
|
|
|
|
def restart_atl(self):
|
|
# The safest way to restart the transform is to rebuild it.
|
|
# Other methods proved to be too finicky...
|
|
|
|
transform = Transform(function=self.atl_partial, anchor=(0.5, 1.0))
|
|
image = At(self, transform) # IMPORTANT: Enable perspective and gl_depth for 3D staging
|
|
|
|
if not renpy.is_init_phase():
|
|
renpy.show(self.name, at_list=[], layer=self.layer, what=image, zorder=self.zorder)
|
|
|
|
# def dynamic(self, st, at):
|
|
# return self.image, None
|
|
|
|
init offset = 5
|
|
|
|
default hooch_chibi = DollChibi(name="hooch", doll=hooch)
|