diff options
Diffstat (limited to 'unrpyc/renpy/display/particle.py')
-rw-r--r-- | unrpyc/renpy/display/particle.py | 615 |
1 files changed, 615 insertions, 0 deletions
diff --git a/unrpyc/renpy/display/particle.py b/unrpyc/renpy/display/particle.py new file mode 100644 index 0000000..cf52417 --- /dev/null +++ b/unrpyc/renpy/display/particle.py @@ -0,0 +1,615 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This code supports sprite and particle animation. + +from renpy.display.render import render, BLIT + +import renpy.display +import random + + +class SpriteCache(renpy.object.Object): + """ + This stores information about a displayble, including the identity + of the displayable, and when it was first displayed. It is also + responsible for caching the displayable surface, so it doesn't + need to be re-rendered. + """ + + # Private Fields: + # + # child - The child displayable. + # + # st - The shown time when this was first displayed, or None if it hasn't + # been rendered. + # + # render - The render of child. + # + # If true, then the render is simple enough it can just be appended to + # the manager's render's children list. + +class Sprite(renpy.object.Object): + """ + :doc: sprites class + + This represents a sprite that is managed by the SpriteManager. It contains + fields that control the placement of the sprite on the screen. Sprites + should not be created directly. Instead, they should be created by + calling :meth:`SpriteManager.create`. + + The fields of a sprite object are: + + `x`, `y` + The x and y coordinates of the upper-left corner of the sprite, + relative to the SpriteManager. + + `zorder` + An integer that's used to control the order of this sprite in the + relative to the other sprites in the SpriteManager. The larger the + number is, the closer to the viewer the sprite is. + + `events` + If True, then events are passed to child. If False, the default, + the children igore events (and hence don't spend time processing + them). + + The methods of a Sprite object are: + """ + + # Fields: + # + # child - the displayable that is the child of this sprite. + # cache - the SpriteCache of child. + # live - True if this sprite is still alive. + # manager - A reference to the SpriteManager. + + def set_child(self, d): + """ + :doc: sprites method + + Changes the Displayable associated with this sprite to `d`. + """ + + id_d = id(d) + + sc = self.manager.displayable_map.get(id_d, None) + if sc is None: + d = renpy.easy.displayable(d) + + sc = SpriteCache() + sc.render = None + sc.child = d + sc.st = None + + self.manager.displayable_map[id_d] = sc + + self.cache = sc + + def destroy(self): + """ + :doc: sprites method + + Destroys this sprite, preventing it from being displayed and + removing it from the SpriteManager. + """ + + self.manager.dead_child = True + self.live = False + self.events = False + + + +class SpriteManager(renpy.display.core.Displayable): + """ + :doc: sprites class + + This displayable manages a collection of sprites, and displays + them at the fastest speed possible. + """ + + def __init__(self, update=None, event=None, predict=None, ignore_time=False, **properties): + """ + `update` + If not None, a function that is called each time a sprite + is rendered by this sprite manager. It is called with one + argument, the time in seconds since this sprite manager + was first displayed. It is expected to return the number + of seconds until the function is called again, and the + SpriteManager is rendered again. + + `event` + If not None, a function that is called when an event occurs. + It takes as arguments: + * A pygame event object. + * The x coordinate of the event. + * The y coordinate of the event. + * The time since the sprite manager was first shown. + If it returns a non-None value, the interaction ends, and + that value is returned. + + `predict` + If not None, a function that returns a list of + displayables. These displayables are predicted when the + sprite manager is. + + `ignore_time` + If True, then time is ignored when rendering displayables. This + should be used when the sprite manager is used with a relatively + small pool of images, and those images do not change over time. + This should only be used with a small number of displayables, as + it will keep all displayables used in memory for the life of the + SpriteManager. + + After being rendered once (before the `update` function is called), + SpriteManagers have the following fields: + + `width`, `height` + + The width and height of this SpriteManager, in pixels. + + + SpriteManagers have the following methods: + """ + + super(SpriteManager, self).__init__(self, **properties) + + self.update_function = update + self.event_function = event + self.predict_function = predict + self.ignore_time = ignore_time + + # A map from a displayable to the SpriteDisplayable object + # representing that displayable. + self.displayable_map = { } + + # A list of children of this displayable, in zorder. (When sorted.) + # This is a list of Sprites. + self.children = [ ] + + # True if at least one child has been killed. + self.dead_child = False + + # True if at least one child responds to events. + self.events = False + + # The width and height. + self.width = None + self.height = None + + def create(self, d): + """ + :doc: sprites method + + Creates a new Sprite for the displayable `d`, and adds it to this + SpriteManager. + """ + + id_d = id(d) + + sc = self.displayable_map.get(id_d, None) + if sc is None: + d = renpy.easy.displayable(d) + + sc = SpriteCache() + sc.render = None + sc.child = d + sc.st = None + self.displayable_map[id_d] = sc + + s = Sprite() + s.x = 0 + s.y = 0 + s.zorder = 0 + s.cache = sc + s.live = True + s.manager = self + s.events = False + + self.children.append(s) + + return s + + def predict_one(self): + if self.predict_function is not None: + for i in self.predict_function(): + renpy.display.predict.displayable(i) + + + def redraw(self, delay=0): + """ + :doc: sprite method + + Causes this SpriteManager to be redrawn in `delay` seconds. + """ + + renpy.display.render.redraw(self, delay) + + def render(self, width, height, st, at): + + self.width = width + self.height = height + + if self.update_function is not None: + + redraw = self.update_function(st) + + if redraw is not None: + renpy.display.render.redraw(self, redraw) + + if not self.ignore_time: + self.displayable_map.clear() + + if self.dead_child: + self.children = [ i for i in self.children if i.live ] + + self.children.sort(key=lambda sc:sc.zorder) + + caches = [ ] + + rv = renpy.display.render.Render(width, height) + + events = False + + for i in self.children: + + events |= i.events + + cache = i.cache + r = i.cache.render + if cache.render is None: + if cache.st is None: + cache.st = st + + cst = st - cache.st + + cache.render = r = render(cache.child, width, height, cst, cst) + cache.fast = (r.operation == BLIT) and (r.forward is None) and (r.alpha == 1.0) + rv.depends_on(r) + + caches.append(cache) + + + if cache.fast: + for child, xo, yo, _focus, _main in r.children: + rv.children.append((child, + xo + i.x, + yo + i.y, + False, + False)) + + else: + rv.subpixel_blit(r, (i.x, i.y)) + + for i in caches: + i.render = None + + return rv + + def event(self, ev, x, y, st): + for i in range(len(self.children) -1, -1, -1): + s = self.children[i] + + if s.events: + rv = s.cache.child.event(ev, x - s.x, y - s.y, st - s.cache.st) + if rv is not None: + return rv + + if self.event_function is not None: + return self.event_function(ev, x, y, st) + else: + return None + + def visit(self): + rv = [ ] + + try: + if self.predict_function: + pl = self.predict_function() + for i in pl: + i = renpy.easy.displayable(i) + rv.append(i) + except: + pass + + return rv + + def destroy_all(self): + self.children = [ ] + + +class Particles(renpy.display.core.Displayable, renpy.python.NoRollback): + """ + Supports particle motion, using the old API. + """ + + __version__ = 1 + + nosave = [ 'particles' ] + + def after_upgrade(self, version): + if version < 1: + self.sm = SpriteManager(update=self.update_callback, predict=self.predict_callback) + + def after_setstate(self): + self.particles = None + + def __init__(self, factory, **properties): + """ + @param factory: A factory object. + """ + + super(Particles, self).__init__(**properties) + + self.sm = SpriteManager(update=self.update_callback, predict=self.predict_callback) + + self.factory = factory + self.particles = None + + def update_callback(self, st): + + particles = self.particles + + if st == 0 or particles is None: + self.sm.destroy_all() + particles = [ ] + + add_parts = self.factory.create(particles, st) + + new_particles = [ ] + + for sprite, p in particles: + update = p.update(st) + + if update is None: + sprite.destroy() + continue + + x, y, _t, d = update + + if d is not sprite.cache.child: + sprite.set_child(d) + + sprite.x = x + sprite.y = y + + new_particles.append((sprite, p)) + + if add_parts: + for p in add_parts: + update = p.update(st) + + if update is None: + continue + + x, y, _t, d = update + + if d is None: + continue + + sprite = self.sm.create(d) + sprite.x = x + sprite.y = y + + new_particles.append((sprite, p)) + + self.particles = new_particles + + return 0 + + def predict_callback(self): + return self.factory.predict() + + def render(self, w, h, st, at): + return renpy.display.render.render(self.sm, w, h, st, at) + +class SnowBlossomFactory(renpy.python.NoRollback): + + rotate = False + + def __setstate__(self, state): + self.start = 0 + vars(self).update(state) + self.init() + + def __init__(self, image, count, xspeed, yspeed, border, start, fast, rotate=False): + self.image = renpy.easy.displayable(image) + self.count = count + self.xspeed = xspeed + self.yspeed = yspeed + self.border = border + self.start = start + self.fast = fast + self.rotate = rotate + self.init() + + def init(self): + self.starts = [ random.uniform(0, self.start) for _i in range(0, self.count) ] # W0201 + self.starts.append(self.start) + self.starts.sort() + + def create(self, particles, st): + + def ranged(n): + if isinstance(n, tuple): + return random.uniform(n[0], n[1]) + else: + return n + + if not particles and self.fast: + rv = [ ] + + for _i in range(0, self.count): + rv.append(SnowBlossomParticle(self.image, + ranged(self.xspeed), + ranged(self.yspeed), + self.border, + st, + random.uniform(0, 100), + fast=True, + rotate=self.rotate)) + return rv + + + if particles is None or len(particles) < self.count: + + # Check to see if we have a particle ready to start. If not, + # don't start it. + if particles and st < self.starts[len(particles)]: + return None + + return [ SnowBlossomParticle(self.image, + ranged(self.xspeed), + ranged(self.yspeed), + self.border, + st, + random.uniform(0, 100), + fast=False, + rotate=self.rotate) ] + + def predict(self): + return [ self.image ] + + +class SnowBlossomParticle(renpy.python.NoRollback): + + def __init__(self, image, xspeed, yspeed, border, start, offset, fast, rotate): + + # safety. + if yspeed == 0: + yspeed = 1 + + self.image = image + self.xspeed = xspeed + self.yspeed = yspeed + self.border = border + self.start = start + self.offset = offset + self.rotate = rotate + + + if not rotate: + sh = renpy.config.screen_height + sw = renpy.config.screen_width + else: + sw = renpy.config.screen_height + sh = renpy.config.screen_width + + + if self.yspeed > 0: + self.ystart = -border + else: + self.ystart = sh + border + + + travel_time = (2.0 * border + sh) / abs(yspeed) + + xdist = xspeed * travel_time + + x0 = min(-xdist, 0) + x1 = max(sw + xdist, sw) + + self.xstart = random.uniform(x0, x1) + + if fast: + self.ystart = random.uniform(-border, sh + border) + self.xstart = random.uniform(0, sw) + + def update(self, st): + to = st - self.start + + xpos = self.xstart + to * self.xspeed + ypos = self.ystart + to * self.yspeed + + if not self.rotate: + sh = renpy.config.screen_height + else: + sh = renpy.config.screen_width + + if ypos > sh + self.border: + return None + + if ypos < -self.border: + return None + + if not self.rotate: + return int(xpos), int(ypos), to + self.offset, self.image + else: + return int(ypos), int(xpos), to + self.offset, self.image + +def SnowBlossom(d, + count=10, + border=50, + xspeed=(20, 50), + yspeed=(100, 200), + start=0, + fast=False, + horizontal=False): + + """ + :doc: sprites_extra + + The snowblossom effect moves multiple instances of a sprite up, + down, left or right on the screen. When a sprite leaves the screen, it + is returned to the start. + + `d` + The displayable to use for the sprites. + + `border` + The size of the border of the screen. The sprite is considered to be + on the screen until it clears the border, ensuring that sprites do + not disappear abruptly. + + `xspeed`, `yspeed` + The speed at which the sprites move, in the horizontal and vertical + directions, respectively. These can be a single number or a tuple of + two numbers. In the latter case, each particle is assigned a random + speed between the two numbers. The speeds can be positive or negative, + as long as the second number in a tuple is larger than the first. + + `start` + The delay, in seconds, before each particle is added. This can be + allows the particles to start at the top of the screen, while not + looking like a "wave" effect. + + `fast` + If true, particles start in the center of the screen, rather than + only at the edges. + + `horizontal` + If true, particles appear on the left or right side of the screen, + rather than the top or bottom. + """ + + # If going horizontal, swap the xspeed and the yspeed. + if horizontal: + xspeed, yspeed = yspeed, xspeed + + return Particles(SnowBlossomFactory(image=d, + count=count, + border=border, + xspeed=xspeed, + yspeed=yspeed, + start=start, + fast=fast, + rotate=horizontal)) + |