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)