diff --git a/pgfw/Audio.py b/pgfw/Audio.py index 2c230e9..8e2bcbd 100644 --- a/pgfw/Audio.py +++ b/pgfw/Audio.py @@ -1,92 +1,50 @@ from os import listdir from os.path import join -from pygame.mixer import Channel, Sound, music, find_channel +from pygame.mixer import Channel, Sound, music, find_channel, get_num_channels from GameChild import * from Input import * class Audio(GameChild): - current_channel = None - paused = False - muted = False + UP, DOWN = .1, -.1 + BASE_VOLUME = .8 def __init__(self, game): GameChild.__init__(self, game) - self.delegate = self.get_delegate() - self.load_fx() + self.original_volumes = {} + self.volume = self.BASE_VOLUME + if self.check_command_line("-mute"): + self.volume = 0 self.subscribe(self.respond) - def load_fx(self): - fx = {} - if self.get_configuration().has_option("audio", "sfx-path"): - root = self.get_resource("audio", "sfx-path") - if root: - for name in listdir(root): - fx[name.split(".")[0]] = Sound(join(root, name)) - self.fx = fx + def set_volume(self, volume=None, increment=None, mute=False): + if mute: + self.volume = 0 + elif increment: + self.volume += increment + if self.volume > 1: + self.volume = 1.0 + elif self.volume < 0: + self.volume = 0 + else: + self.volume = volume def respond(self, event): - if self.delegate.compare(event, "mute"): - self.mute() + compare = self.get_game().delegate.compare + if compare(event, "volume-mute"): + self.set_volume(mute=True) + elif compare(event, "volume-up"): + self.set_volume(increment=self.UP) + elif compare(event, "volume-down"): + self.set_volume(increment=self.DOWN) - def mute(self): - self.muted = True - self.set_volume() - - def unmute(self): - self.muted = False - self.set_volume() - - def set_volume(self): - volume = int(not self.muted) - music.set_volume(volume) - if self.current_channel: - self.current_channel.set_volume(volume) - - def play_bgm(self, path, stream=False): - self.stop_current_channel() - if stream: - music.load(path) - music.play(-1) - else: - self.current_channel = Sound(path).play(-1) - self.set_volume() - - def stop_current_channel(self): - music.stop() - if self.current_channel: - self.current_channel.stop() - self.current_channel = None - self.paused = False - - def play_fx(self, name, panning=.5): - if not self.muted: - channel = find_channel(True) - if panning != .5: - offset = 1 - abs(panning - .5) * 2 - if panning < .5: - channel.set_volume(1, offset) - else: - channel.set_volume(offset, 1) - channel.play(self.fx[name]) - - def pause(self): - channel = self.current_channel - paused = self.paused - if paused: - music.unpause() - if channel: - channel.unpause() - else: - music.pause() - if channel: - channel.pause() - self.paused = not paused - - def is_bgm_playing(self): - current = self.current_channel - if current and current.get_sound(): - return True - return music.get_busy() + def update(self): + for ii in xrange(get_num_channels()): + channel = Channel(ii) + sound = channel.get_sound() + if sound is not None: + if sound not in self.original_volumes.keys(): + self.original_volumes[sound] = sound.get_volume() + sound.set_volume(self.original_volumes[sound] * self.volume) diff --git a/pgfw/Configuration.py b/pgfw/Configuration.py index 4d16b93..df1d279 100644 --- a/pgfw/Configuration.py +++ b/pgfw/Configuration.py @@ -65,6 +65,7 @@ class Configuration(RawConfigParser): set_option(section, "windows-dist-path", "dist/win/", False) set_option(section, "windows-icon-path", "", False) set_option(section, "lowercase-boolean-true", "yes", False) + set_option(section, "osx-includes", "", False) section = "display" add_section(section) set_option(section, "dimensions", "480, 360", False) @@ -106,15 +107,17 @@ class Configuration(RawConfigParser): set_option(section, "double-click-time-limit", ".5", False) section = "keys" add_section(section) - set_option(section, "up", "K_UP, K_w", False) - set_option(section, "right", "K_RIGHT, K_d", False) - set_option(section, "down", "K_DOWN, K_s", False) - set_option(section, "left", "K_LEFT, K_a", False) + set_option(section, "up", "K_UP", False) + set_option(section, "right", "K_RIGHT", False) + set_option(section, "down", "K_DOWN", False) + set_option(section, "left", "K_LEFT", False) set_option(section, "capture-screen", "K_F9", False) set_option(section, "toggle-fullscreen", "K_F11", False) set_option(section, "reset-game", "K_F8", False) set_option(section, "record-video", "K_F10", False) - set_option(section, "mute", "K_F12", False) + set_option(section, "volume-down", "K_F1", False) + set_option(section, "volume-up", "K_F2", False) + set_option(section, "volume-mute", "K_F3", False) set_option(section, "toggle-interpolator", "K_F7", False) section = "joy" add_section(section) @@ -295,10 +298,11 @@ class Configuration(RawConfigParser): exclude = [] if self.has_option(section, option): exclude = self.get(section, option) - exclude += [".git", ".gitignore", "README", "build/", "dist/", - "setup.py", "MANIFEST", "PKG-INFO", + exclude += [".git*", "README", "build/", "dist/", "*.egg-info", + "*.py", "MANIFEST*", "PKG-INFO", "*.pyc", "*.swp", "*~", self.get("setup", "changelog"), - self.get("setup", "package-root")] + self.get("setup", "package-root"), + self.get("setup", "init-script")] for location in self.get("setup", "additional-packages"): exclude.append(location) self.set(section, option, exclude, False) @@ -395,7 +399,7 @@ class TypeDeclarations(dict): "setup": {"list": ["classifiers", "resource-search-path", "requirements", "data-exclude", - "additional-packages"], + "additional-packages", "osx-includes"], "path": ["installation-dir", "changelog", "description-file", "main-object", "icon-path", "windows-dist-path", diff --git a/pgfw/Delegate.py b/pgfw/Delegate.py index bf9c65b..8b49587 100644 --- a/pgfw/Delegate.py +++ b/pgfw/Delegate.py @@ -79,7 +79,7 @@ class Delegate(GameChild): return self.get_command_attribute(evt) in commands def get_command_attribute(self, evt): - return evt.dict[self.command_key] + return evt.dict.has_key(self.command_key) and evt.dict[self.command_key] def post(self, command=None, cancel=False, **attributes): attributes[self.command_key] = command diff --git a/pgfw/Game.py b/pgfw/Game.py index b1402cc..ae7f98b 100644 --- a/pgfw/Game.py +++ b/pgfw/Game.py @@ -54,6 +54,7 @@ class Game(GameChild): self.update() else: self.interpolator.gui.update() + self.audio.update() if self.video_recorder.requested: self.video_recorder.update() diff --git a/pgfw/Input.py b/pgfw/Input.py index d1fc2ad..2f1496a 100644 --- a/pgfw/Input.py +++ b/pgfw/Input.py @@ -94,7 +94,7 @@ class Input(GameChild): if not self.suppressed and not self.check_command_line("-disable-joy-axis"): axis = event.axis value = event.value - if not value: + if -.01 < value < .01: for command in "up", "right", "down", "left": self.post_command(command, cancel=True) if command not in self.any_press_ignored: diff --git a/pgfw/Interpolator.py b/pgfw/Interpolator.py index dc37617..3460a56 100644 --- a/pgfw/Interpolator.py +++ b/pgfw/Interpolator.py @@ -404,7 +404,7 @@ class GUI(Animation): def deactivate(self): self.active = False self.time_filter.open() - self.audio.muted = self.saved_mute_state + # self.audio.muted = self.saved_mute_state self.display.set_mouse_visibility(self.saved_mouse_state) def respond_to_command(self, event): @@ -427,8 +427,8 @@ class GUI(Animation): def activate(self): self.active = True self.time_filter.close() - self.saved_mute_state = self.audio.muted - self.audio.mute() + # self.saved_mute_state = self.audio.muted + # self.audio.mute() self.draw() self.saved_mouse_state = self.display.set_mouse_visibility(True) diff --git a/pgfw/Note.py b/pgfw/Note.py index 1fa80d3..85d13be 100644 --- a/pgfw/Note.py +++ b/pgfw/Note.py @@ -138,3 +138,7 @@ class Chord: fade_in = [fade_in] for ii, note in enumerate(self.notes): note.play(maxtime, fadeout[ii % len(fadeout)], panning, fade_in[ii % len(fade_in)]) + + def stop(self): + for note in self.notes: + note.stop() diff --git a/pgfw/Setup.py b/pgfw/Setup.py index 04cf753..170428e 100644 --- a/pgfw/Setup.py +++ b/pgfw/Setup.py @@ -1,11 +1,12 @@ from os import walk, remove -from os.path import sep, join, exists, normpath +from os.path import sep, join, exists, normpath, basename from re import findall, sub from distutils.core import setup from distutils.command.install import install from pprint import pprint from fileinput import FileInput from re import sub, match +from fnmatch import fnmatch from Configuration import * @@ -29,7 +30,8 @@ class Setup: for location in locations: if exists(location): for root, dirs, files in walk(location, followlinks=True): - packages.append(root.replace(sep, ".")) + if exists(join(root, "__init__.py")): + packages.append(root.replace(sep, ".")) return packages def build_data_map(self): @@ -48,12 +50,16 @@ class Setup: def remove_excluded(self, paths, root, exclude): removal = [] for path in paths: - if normpath(join(root, path)) in exclude: + if self.contains_path(join(root, path), exclude): removal.append(path) for path in removal: - paths.remove(path) + if path in paths: + paths.remove(path) return paths + def contains_path(self, path, container): + return any(fnmatch(path, rule) or fnmatch(basename(path), rule) for rule in container) + def translate_title(self): config = self.config.get_section("setup") title = config["title"].replace(" ", config["whitespace-placeholder"]) diff --git a/pgfw/SetupOSX.py b/pgfw/SetupOSX.py new file mode 100644 index 0000000..9ef004c --- /dev/null +++ b/pgfw/SetupOSX.py @@ -0,0 +1,27 @@ +from setuptools import setup, find_packages +from Configuration import Configuration +from Setup import Setup + +class SetupOSX(Setup): + + def __init__(self): + Setup.__init__(self) + + def setup(self): + config = Configuration() + setup_section = config.get_section("setup") + version = setup_section["version"] + name = self.translate_title() + plist = dict( + CFBundleIconFile=name, + CFBundleName=name, + CFBundleShortVersionString=version, + CFBundleGetInfoString=' '.join([name, version]), + CFBundleExecutable=name, + CFBundleIdentifier='org.' + name.lower()) + setup(name=name, + version=version, + app=[dict(script=setup_section["init-script"], plist=plist)], + setup_requires=["py2app"], + options=dict(py2app=dict(arch="i386",)), + data_files=setup_section["osx-includes"]) diff --git a/pgfw/Sprite.py b/pgfw/Sprite.py index 5a3da70..b7cebce 100644 --- a/pgfw/Sprite.py +++ b/pgfw/Sprite.py @@ -9,21 +9,25 @@ from pygame.transform import flip from pygame.locals import * from Animation import Animation -from Vector import Vector +from Vector import Vector, EVector +from extension import get_hue_shifted_surface, get_step class Sprite(Animation): - def __init__(self, parent, framerate=None): + def __init__(self, parent, framerate=None, neighbors=[], mass=None): Animation.__init__(self, parent, self.shift_frame, framerate) self.frames = [] self.mirrored = False self.alpha = 255 self.locations = [] self.framesets = [Frameset(self, framerate=framerate)] + self.frameset_index = 0 + self.neighbors = neighbors + self.mass = mass + self.step = EVector() self.set_frameset(0) self.locations.append(Location(self)) self.motion_overflow = Vector() - self.stop() self.display_surface = self.get_display_surface() def __getattr__(self, name): @@ -39,11 +43,13 @@ class Sprite(Animation): if frameset.name == identifier: identifier = ii break - self.frameset_index = identifier - self.register_interval() - self.update_location_size() - if self.get_current_frameset().length() > 1: - self.play() + if self.frameset_index != identifier: + self.frameset_index = identifier + self.register_interval() + self.update_location_size() + if self.get_current_frameset().length() > 1: + self.play() + self.get_current_frameset().reset() def register_interval(self): self.register(self.shift_frame, @@ -204,21 +210,26 @@ class Sprite(Animation): for frameset in self.framesets: frameset.reverse() - def go(self, dx=0, dy=0): - self.go_vector = Vector(dx, dy) + def set_step(self, dx=0, dy=0, magnitude=None, angle=0): + self.step.set_step(dx, dy, magnitude, angle) - def stop(self): - self.go_vector = Vector() + def cancel_step(self): + self.step.set_step(0, 0) - def is_going(self): - return self.go_vector != [0, 0] + def is_stepping(self): + return bool(self.step) def update(self, areas=None, substitute=None): Animation.update(self) - if self.is_going(): - self.move(*self.go_vector) + if self.is_stepping(): + self.move(self.step.dx, self.step.dy) + for neighbor in self.neighbors: + self.move(*get_step(self.location.center, neighbor.location.center, + neighbor.mass)) if self.get_current_frameset().length(): self.draw(areas, substitute) + # for location in self.locations: + # location.update() def draw(self, areas=None, substitute=None): for location in self.locations: @@ -249,6 +260,10 @@ class Location(Rect): overflow[1] -= int(overflow[1]) return excess + def move_to(self, x, y, base=None): + ox, oy = self.apply_motion_overflow(base) + self.move_ip(x - ox, y - oy) + def reset_motion_overflow(self): self.motion_overflow.place_at_origin() @@ -269,6 +284,18 @@ class Location(Rect): def is_hidden(self): return self.hidden + def update(self): + pass + # for neighbor in self.sprite.neighbors: + # if neighbor.mass: + # closest = neighbor.location + # for location in neighbor.locations[1:]: + # if get_distance(self.center, location.center) < \ + # get_distance(self.center, closest.center): + # closest = location + # self.move_ip(get_step(self.center, closest.center, + # neighbor.mass)) + class Fader(Surface): @@ -427,3 +454,25 @@ class Frameset: def reverse(self): self.reversed = not self.reversed + + +class BlinkingSprite(Sprite): + + def __init__(self, parent, blink_rate, framerate=None): + Sprite.__init__(self, parent, framerate) + self.register(self.blink, interval=blink_rate) + self.play(self.blink) + + def reset(self): + self.unhide() + + def blink(self): + self.toggle_hidden() + + +class RainbowSprite(Sprite): + + def __init__(self, parent, image, hue_shift=8, framerate=None): + Sprite.__init__(self, parent, framerate) + for hue in xrange(0, 360, hue_shift): + self.add_frame(get_hue_shifted_surface(image, hue)) diff --git a/pgfw/TimeFilter.py b/pgfw/TimeFilter.py index e57e3c1..d819a16 100644 --- a/pgfw/TimeFilter.py +++ b/pgfw/TimeFilter.py @@ -6,9 +6,12 @@ class TimeFilter(GameChild): def __init__(self, parent): GameChild.__init__(self, parent) - self.ticks = self.unfiltered_ticks = self.last_ticks = get_ticks() + self.reset_ticks() self.open() + def reset_ticks(self): + self.ticks = self.unfiltered_ticks = self.last_ticks = get_ticks() + def close(self): self.closed = True diff --git a/pgfw/Vector.py b/pgfw/Vector.py index 819e847..f14c807 100644 --- a/pgfw/Vector.py +++ b/pgfw/Vector.py @@ -1,8 +1,18 @@ +from math import pi, degrees + +from extension import get_delta, get_distance, get_angle + class Vector(list): def __init__(self, x=0, y=0): list.__init__(self, (x, y)) + def __repr__(self): + message = "<%f" % self[0] + for value in self[1:]: + message += ", %f" % value + return message + ">" + def __getattr__(self, name): if name == "x": return self[0] @@ -48,6 +58,24 @@ class Vector(list): self.y *= other return self + def __eq__(self, other): + for sv, ov in zip(self, other): + if value != other[ii]: + return False + return True + + def __ne__(self, other): + for sv, ov in zip(self, other): + if value == other[ii]: + return False + return True + + def __nonzero__(self): + for value in self: + if bool(value): + return True + return False + def apply_to_components(self, function): self.x = function(self.x) self.y = function(self.y) @@ -67,3 +95,37 @@ class Vector(list): def place_at_origin(self): self.x = 0 self.y = 0 + + +class EVector(Vector): + + def __init__(self, x=0, y=0, dx=0, dy=0, magnitude=None, angle=0): + Vector.__init__(self, x, y) + self.set_step(dx, dy, magnitude, angle) + + def set_step(self, dx=0, dy=0, magnitude=None, angle=0): + """specify angle in radians, counter-clockwise, 0 is up""" + if magnitude is not None: + self.magnitude = magnitude + self.angle = angle + self.dx, self.dy = get_delta(angle, magnitude, False) + else: + self.dx = dx + self.dy = dy + if dx == 0 and dy == 0: + self.magnitude = 0 + self.angle = 0 + else: + end = self.x + dx, self.y + dy + self.magnitude = get_distance(self, end) + self.angle = -get_angle(self, end) - pi + + def __repr__(self): + return "" % \ + (self.dx, self.dy, self.magnitude, self.angle) + + def __nonzero__(self): + return bool(self.magnitude) + + def move(self): + self += self.dx, self.dy diff --git a/pgfw/extension.py b/pgfw/extension.py index 7a4b7ef..d7531ea 100644 --- a/pgfw/extension.py +++ b/pgfw/extension.py @@ -1,6 +1,10 @@ from random import randint, random from math import sin, cos, atan2, radians, sqrt, pi +from pygame import Surface, PixelArray, Color +from pygame.mixer import get_num_channels, Channel +from pygame.locals import * + def get_step(start, end, speed): x0, y0 = start x1, y1 = end @@ -13,14 +17,15 @@ def get_angle(start, end, transpose=False): angle = -angle - pi return angle -def get_endpoint(start, angle, magnitude): +def get_endpoint(start, angle, magnitude, translate_angle=True): """clockwise, 0 is up""" x0, y0 = start - dx, dy = get_delta(angle, magnitude) + dx, dy = get_delta(angle, magnitude, translate_angle) return x0 + dx, y0 + dy -def get_delta(angle, magnitude): - angle = radians(angle) +def get_delta(angle, magnitude, translate_angle=True): + if translate_angle: + angle = radians(angle) return sin(angle) * magnitude, -cos(angle) * magnitude def rotate_2d(point, center, angle, translate_angle=True): @@ -65,6 +70,7 @@ def place_in_rect(rect, incoming, contain=True, *args): if not collides: break +# from http://www.realtimerendering.com/resources/GraphicsGems/gemsii/xlines.c def get_intersection(p0, p1, p2, p3): x0, y0 = p0 x1, y1 = p1 @@ -112,3 +118,95 @@ def get_value_in_range(start, end, position, reverse=False): if reverse: position = 1 - position return (end - start) * position + start + +def render_box(font, text, antialias, color, background=None, border=None, + border_width=1, padding=0): + surface = font.render(text, antialias, color, background) + if padding: + if isinstance(padding, int): + padding = [padding] * 2 + padding = [x * 2 for x in padding] + rect = surface.get_rect() + padded_surface = Surface(rect.inflate(padding).size, SRCALPHA) + if background is not None: + padded_surface.fill(background) + rect.center = padded_surface.get_rect().center + padded_surface.blit(surface, rect) + surface = padded_surface + if border is not None: + if isinstance(border_width, int): + border_width = [border_width] * 2 + border_width = [x * 2 for x in border_width] + rect = surface.get_rect() + bordered_surface = Surface(rect.inflate(border_width).size) + bordered_surface.fill(border) + rect.center = bordered_surface.get_rect().center + bordered_surface.blit(surface, rect) + surface = bordered_surface + return surface + +def get_color_swapped_surface(surface, current, replacement): + swapped = surface.copy() + pixels = PixelArray(swapped) + pixels.replace(current, replacement) + del pixels + return swapped + +def get_busy_channel_count(): + count = 0 + for index in xrange(get_num_channels()): + count += Channel(index).get_busy() + return count + +def get_hue_shifted_surface(base, offset): + surface = base.copy() + pixels = PixelArray(surface) + color = Color(0, 0, 0) + for x in xrange(surface.get_width()): + for y in xrange(surface.get_height()): + h, s, l, a = Color(*surface.unmap_rgb(pixels[x][y])).hsla + if a: + color.hsla = (h + offset) % 360, s, l, a + pixels[x][y] = color + del pixels + return surface + +def get_inverted_surface(base): + surface = base.copy() + pixels = PixelArray(surface) + for x in xrange(surface.get_width()): + for y in xrange(surface.get_height()): + color = Color(*surface.unmap_rgb(pixels[x][y])) + if color.hsla[3]: + color.r = 255 - color.r + color.g = 255 - color.g + color.b = 255 - color.b + pixels[x][y] = color + del pixels + return surface + +def fill_tile(surface, tile): + for x in xrange(0, surface.get_width(), tile.get_width()): + for y in xrange(0, surface.get_height(), tile.get_height()): + surface.blit(tile, (x, y)) + +def get_shadowed_text(text, font, offset, color, antialias=True, shadow_color=(0, 0, 0), + colorkey=(255, 0, 255)): + foreground = font.render(text, antialias, color) + background = font.render(text, antialias, shadow_color) + alpha = SRCALPHA if antialias else 0 + surface = Surface((foreground.get_width() + abs(offset[0]), + foreground.get_height() + abs(offset[1])), alpha) + if not antialias: + surface.set_colorkey(colorkey) + surface.fill(colorkey) + surface.blit(background, ((abs(offset[0]) + offset[0]) / 2, + (abs(offset[1]) + offset[1]) / 2)) + surface.blit(foreground, ((abs(offset[0]) - offset[0]) / 2, + (abs(offset[1]) - offset[1]) / 2)) + return surface + +def get_hsla_color(hue, saturation=100, lightness=50, alpha=100): + color = Color(0, 0, 0, 0) + color.hsla = hue % 360, saturation, lightness, alpha + return color diff --git a/pgfw/gfx_extension.py b/pgfw/gfx_extension.py new file mode 100644 index 0000000..9e5aba2 --- /dev/null +++ b/pgfw/gfx_extension.py @@ -0,0 +1,14 @@ +from pygame.gfxdraw import (aacircle, filled_circle, aatrigon, filled_trigon, + aapolygon, filled_polygon) + +def aa_filled_circle(surface, cx, cy, radius, color): + aacircle(surface, cx, cy, radius, color) + filled_circle(surface, cx, cy, radius, color) + +def aa_filled_trigon(surface, x1, y1, x2, y2, x3, y3, color): + aatrigon(surface, x1, y1, x2, y2, x3, y3, color) + filled_trigon(surface, x1, y1, x2, y2, x3, y3, color) + +def aa_filled_polygon(surface, points, color): + aapolygon(surface, points, color) + filled_polygon(surface, points, color)