From cf8021c48021e8b1cbdadfc97c886eb7ea61922c Mon Sep 17 00:00:00 2001 From: Frank DeMarco Date: Fri, 1 May 2020 02:26:12 -0400 Subject: [PATCH] - confirm quit - bgm added to audio panel - delegate maintains order subscribers are added by object - optionally suppress "any key" event when mods are pressed - dict of named child objects added to sprites - generate glow animation effect frames --- pgfw/Animation.py | 2 + pgfw/Audio.py | 252 +++++++++++++++++++++++++++-------- pgfw/Configuration.py | 12 +- pgfw/Delegate.py | 4 +- pgfw/Game.py | 51 ++++++- pgfw/Input.py | 16 ++- pgfw/Sprite.py | 32 ++++- pgfw/confirm_quit_message.py | 10 ++ pgfw/extension.py | 31 ++++- 9 files changed, 333 insertions(+), 77 deletions(-) create mode 100644 pgfw/confirm_quit_message.py diff --git a/pgfw/Animation.py b/pgfw/Animation.py index 74913aa..7fd817b 100644 --- a/pgfw/Animation.py +++ b/pgfw/Animation.py @@ -38,6 +38,8 @@ class Animation(GameChild): def get_default(self, method=None): if not method: method = self.default_method + elif isinstance(method, str): + return getattr(self, method) return method def halt(self, method=None): diff --git a/pgfw/Audio.py b/pgfw/Audio.py index 3e8df6b..7c0b4c2 100644 --- a/pgfw/Audio.py +++ b/pgfw/Audio.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -import os, re, shutil, pygame +import os, re, shutil, pygame, sys from .GameChild import * from .Sprite import * @@ -23,6 +23,8 @@ class Audio(GameChild): self.subscribe(self.respond) self.sfx = {} self.load_sfx() + self.bgm = {} + self.load_bgm() def set_volume(self, volume=None, increment=None, mute=False): if mute: @@ -94,7 +96,7 @@ class Audio(GameChild): def load_sfx_file(self, path, name=None, replace=False, prefix="", volume=1.0, fade_out=0, loops=0, maxtime=0): path = self.get_resource(path) - if path and path.split(".")[-1] in self.get_configuration("audio", "sfx-extensions"): + if path and self.is_sound_file(path): if name is None: name = prefix + re.sub("\.[^.]*$", "", os.path.basename(path)) if not replace and name in self.sfx: @@ -106,6 +108,57 @@ class Audio(GameChild): return True return False + # + # Loading BGM procedure + # + # - load config file name/path definitions + # - check project specific bgm paths, load any that don't conflict + # - other paths can replace loaded paths through the audio panel and get + # written to config file + # + def load_bgm(self): + for name, path in self.get_configuration("bgm").items(): + self.set_bgm(path, name) + for root in self.get_configuration("audio", "bgm-project-path"): + if os.path.exists(root): + print("checking {} for music".format(root)) + if os.path.isfile(root): + self.set_bgm(root) + else: + for node, branches, leaves in os.walk(root, followlinks=True): + for leaf in leaves: + prefix = re.sub(root, "", node) + prefix = re.sub("^/", "", prefix) + if prefix: + prefix = re.sub("/", "_", prefix) + "_" + self.set_bgm(os.path.join(node, leaf), prefix=prefix) + + def set_bgm(self, path, name=None, prefix=""): + print("setting {} music to {}".format(name, path)) + try: + pygame.mixer.music.load(path) + except: + print("can't load {} as music".format(path)) + return False + if name is None: + name = os.path.basename(path).split(".")[0] + self.bgm[prefix + name] = path + return True + + def play_bgm(self, name): + pygame.mixer.music.load(self.bgm[name]) + pygame.mixer.music.play(-1) + + def is_sound_file(self, path): + return path.split(".")[-1] in self.get_configuration("audio", "sfx-extensions") + + def is_loadable(self, path): + try: + pygame.mixer.Sound(path) + except: + return False + return True + def update(self): # for ii in range(pygame.mixer.get_num_channels()): # channel = pygame.mixer.Channel(ii) @@ -231,9 +284,7 @@ class AudioPanel(Animation): self.active = False def respond(self, event): - if self.get_delegate().compare(event, "toggle-audio-panel") and \ - pygame.key.get_mods() & KMOD_CTRL and pygame.key.get_mods() & KMOD_SHIFT and \ - self.get_audio().sfx: + if self.get_delegate().compare(event, "toggle-audio-panel") and self.get_audio().sfx: if self.active: self.deactivate() else: @@ -246,12 +297,16 @@ class AudioPanel(Animation): self.row_offset += 1 elif event.button == 4: self.row_offset -= 1 + elif event.button == 3: + self.deactivate() def build(self): for row in self.rows: row.unsubscribe() del row self.rows = [] + for key in sorted(self.parent.bgm): + self.rows.append(AudioPanelRow(self, key, True)) for key in sorted(self.parent.sfx): self.rows.append(AudioPanelRow(self, key)) @@ -285,42 +340,52 @@ class AudioPanelRow(BlinkingSprite): SLIDER_W = 60 BUTTON_W = 30 - def __init__(self, parent, key): + def __init__(self, parent, key, is_bgm=False): BlinkingSprite.__init__(self, parent, 500) self.key = key self.selected = False self.font = self.parent.font_large + self.is_bgm = is_bgm self.build() - font_medium = self.parent.font_medium - font_small = self.parent.font_small - self.volume_spinner = AudioPanelSpinner( - self, font_medium, font_small, self.SLIDER_W, self.location.h, .05, - self.get_sound_effect().local_volume, - self.get_sound_effect().adjust_volume, self.FOREGROUND, - self.BACKGROUND, 2, "vol") - self.fade_out_spinner = AudioPanelSpinner( - self, font_medium, font_small, self.SLIDER_W, self.location.h, .1, - self.get_sound_effect().fade_out_length, - self.get_sound_effect().adjust_fade_out_length, self.FOREGROUND, - self.BACKGROUND, 1, "fade") - self.loops_spinner = AudioPanelSpinner( - self, font_medium, font_small, self.SLIDER_W, self.location.h, 1, - self.get_sound_effect().loops, - self.get_sound_effect().adjust_loop_count, self.FOREGROUND, - self.BACKGROUND, 0, "loops") - self.maxtime_spinner = AudioPanelSpinner( - self, font_medium, font_small, self.SLIDER_W, self.location.h, .1, - self.get_sound_effect().maxtime, - self.get_sound_effect().adjust_maxtime, self.FOREGROUND, - self.BACKGROUND, 1, "cutoff") - self.play_button = AudioPanelButton(self, self.get_sound_effect().play) + if not self.is_bgm: + font_medium = self.parent.font_medium + font_small = self.parent.font_small + self.volume_spinner = AudioPanelSpinner( + self, font_medium, font_small, self.SLIDER_W, self.location.h, .05, + self.get_sound_effect().local_volume, + self.get_sound_effect().adjust_volume, self.FOREGROUND, + self.BACKGROUND, 2, "vol") + self.fade_out_spinner = AudioPanelSpinner( + self, font_medium, font_small, self.SLIDER_W, self.location.h, .1, + self.get_sound_effect().fade_out_length, + self.get_sound_effect().adjust_fade_out_length, self.FOREGROUND, + self.BACKGROUND, 1, "fade") + self.loops_spinner = AudioPanelSpinner( + self, font_medium, font_small, self.SLIDER_W, self.location.h, 1, + self.get_sound_effect().loops, + self.get_sound_effect().adjust_loop_count, self.FOREGROUND, + self.BACKGROUND, 0, "loops") + self.maxtime_spinner = AudioPanelSpinner( + self, font_medium, font_small, self.SLIDER_W, self.location.h, .1, + self.get_sound_effect().maxtime, + self.get_sound_effect().adjust_maxtime, self.FOREGROUND, + self.BACKGROUND, 1, "cutoff") + if self.is_bgm: + callback, kwargs = self.get_game().get_audio().play_bgm, {"name": self.key} + else: + callback, kwargs = self.get_sound_effect().play, {} + self.play_button = AudioPanelButton(self, callback, kwargs) frame = pygame.Surface((self.BUTTON_W, self.location.h), SRCALPHA) frame.fill(self.BACKGROUND) stop_button_frame = frame.copy() w, h = frame.get_size() pygame.draw.polygon(frame, self.FOREGROUND, ((w * .25, h * .25), (w * .25, h * .75), (w * .75, h * .5))) self.play_button.add_frame(frame) - self.stop_button = AudioPanelButton(self, self.get_sound_effect().stop) + if self.is_bgm: + callback = pygame.mixer.music.stop + else: + callback = self.get_sound_effect().stop + self.stop_button = AudioPanelButton(self, callback) stop_button_frame.fill(self.FOREGROUND, (w * .25, h * .25, w * .5, h * .5)) self.stop_button.add_frame(stop_button_frame) self.stop_blinking() @@ -346,8 +411,9 @@ class AudioPanelRow(BlinkingSprite): GameChild.unsubscribe(self, self.respond, pygame.MOUSEBUTTONDOWN) self.play_button.unsubscribe() self.stop_button.unsubscribe() - for spinner in self.volume_spinner, self.fade_out_spinner, self.loops_spinner, self.maxtime_spinner: - spinner.unsubscribe() + if not self.is_bgm: + for spinner in self.volume_spinner, self.fade_out_spinner, self.loops_spinner, self.maxtime_spinner: + spinner.unsubscribe() def build(self): ds = self.get_display_surface() @@ -374,7 +440,11 @@ class AudioPanelRow(BlinkingSprite): file_sprite.location.midright = self.location.right - self.INDENT, self.location.centery file_sprite.display_surface = surface file_name_sprite = Sprite(self) - file_name_text = self.font.render(self.get_sound_effect().path, True, self.FOREGROUND) + if self.is_bgm: + file_name = self.get_bgm() + else: + file_name = self.get_sound_effect().path + file_name_text = self.font.render(file_name, True, self.FOREGROUND) file_name_sprite.add_frame(file_name_text) file_name_sprite.display_surface = box file_name_sprite.location.midright = file_sprite.location.w - self.INDENT, file_sprite.location.h / 2 @@ -384,31 +454,43 @@ class AudioPanelRow(BlinkingSprite): def get_sound_effect(self): return self.get_game().get_audio().sfx[self.key] + def get_bgm(self): + return self.get_game().get_audio().bgm[self.key] + def update_config(self): - if not self.get_configuration().has_section("sfx"): - self.get_configuration().add_section("sfx") - sound_effect = self.get_sound_effect() - self.get_configuration().set( - "sfx", self.key, "{}, {:.2f}, {:.2f}, {}, {:.2f}".format( + if self.is_bgm: + section_name = "bgm" + else: + section_name = "sfx" + if not self.get_configuration().has_section(section_name): + self.get_configuration().add_section(section_name) + if self.is_bgm: + config_value = self.get_bgm() + else: + sound_effect = self.get_sound_effect() + config_value = "{}, {:.2f}, {:.2f}, {}, {:.2f}".format( sound_effect.path, sound_effect.local_volume, sound_effect.fade_out_length, - sound_effect.loops, sound_effect.maxtime)) + sound_effect.loops, sound_effect.maxtime) + self.get_configuration().set(section_name, self.key, config_value) config_path = self.get_configuration().locate_project_config_file() backup_path = config_path + ".backup" shutil.copyfile(config_path, backup_path) self.get_configuration().write(open(config_path, "w")) def update(self): - self.volume_spinner.location.midleft = self.location.move(5, 0).midright - self.fade_out_spinner.location.midleft = self.volume_spinner.location.midright - self.loops_spinner.location.midleft = self.fade_out_spinner.location.midright - self.maxtime_spinner.location.midleft = self.loops_spinner.location.midright - self.play_button.location.midleft = self.maxtime_spinner.location.move(5, 0).midright + self.play_button.location.midleft = self.location.move(5, 0).midright self.stop_button.location.midleft = self.play_button.location.midright + if not self.is_bgm: + self.volume_spinner.location.midleft = self.stop_button.location.move(5, 0).midright + self.fade_out_spinner.location.midleft = self.volume_spinner.location.midright + self.loops_spinner.location.midleft = self.fade_out_spinner.location.midright + self.maxtime_spinner.location.midleft = self.loops_spinner.location.midright Sprite.update(self) - self.volume_spinner.update() - self.fade_out_spinner.update() - self.loops_spinner.update() - self.maxtime_spinner.update() + if not self.is_bgm: + self.volume_spinner.update() + self.fade_out_spinner.update() + self.loops_spinner.update() + self.maxtime_spinner.update() self.play_button.update() self.stop_button.update() @@ -422,7 +504,9 @@ class AudioPanelFileBrowser(Sprite): def __init__(self, parent): Sprite.__init__(self, parent) + self.rows = [] self.font = self.parent.font_large + self.previewing_sound = None ds = self.get_display_surface() dsr = ds.get_rect() surface = pygame.Surface((dsr.w * self.WIDTH - 2, dsr.h * self.HEIGHT - 2), SRCALPHA) @@ -434,7 +518,9 @@ class AudioPanelFileBrowser(Sprite): self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN) def reset(self): - self.visit(self.HOME) + if self.previewing_sound is not None: + self.previewing_sound.stop() + # self.visit(self.HOME) self.hide() def respond(self, event): @@ -442,14 +528,20 @@ class AudioPanelFileBrowser(Sprite): if event.button == 1: if self.collide(event.pos): for row in self.rows: - if row.collide(Vector(*event.pos).get_moved(-self.location.left, -self.location.top)): + pos = Vector(*event.pos).get_moved(-self.location.left, -self.location.top) + if (not row.has_child("button") or pos.x < row.get_child("button").location.left) and row.collide(pos): full_path = os.path.join(os.path.sep.join(self.trail), row.path) if row.path == self.HOME or row.path == self.UP or \ os.path.isdir(full_path) and os.access(full_path, os.R_OK): self.visit(row.path) elif os.path.isfile(full_path) and os.access(full_path, os.R_OK): + loaded = False selected = self.parent.get_selected() - if self.get_audio().load_sfx_file(full_path, selected.key, True): + if selected.is_bgm: + loaded = self.get_audio().set_bgm(full_path, selected.key) + else: + loaded = self.get_audio().load_sfx_file(full_path, selected.key, True) + if loaded: selected.update_config() self.hide() self.get_delegate().cancel_propagation() @@ -466,8 +558,24 @@ class AudioPanelFileBrowser(Sprite): for row in self.parent.rows: row.selected = False row.stop_blinking() + row.play_button.set_clickable() + row.stop_button.set_clickable() + for row in self.rows: + if row.has_child("button"): + row.get_child("button").set_clickable(False) + if self.previewing_sound is not None: + self.previewing_sound.stop() Sprite.hide(self) + def unhide(self): + for row in self.parent.rows: + row.play_button.set_clickable(False) + row.stop_button.set_clickable(False) + for row in self.rows: + if row.has_child("button"): + row.get_child("button").set_clickable() + Sprite.unhide(self) + def visit(self, path): if path == self.UP and len(self.trail) > 1: path = self.trail[-2] @@ -478,7 +586,8 @@ class AudioPanelFileBrowser(Sprite): if path == self.HOME: self.trail = [] self.paths = ["/"] - for option in "sfx-repository-path", "sfx-default-path", "sfx-project-path": + for option in "sfx-repository-path", "sfx-default-path", "sfx-project-path", \ + "bgm-repository-path", "bgm-project-path": for sfx_location in self.get_configuration("audio", option): if self.get_resource(sfx_location): self.paths.append(self.get_resource(sfx_location)) @@ -491,6 +600,10 @@ class AudioPanelFileBrowser(Sprite): self.build() def build(self): + for row in self.rows: + if row.has_child("button"): + row.get_child("button").unsubscribe() + del row self.rows = [] for path in self.paths: row = Sprite(self) @@ -498,10 +611,29 @@ class AudioPanelFileBrowser(Sprite): text = self.font.render(path, True, self.COLORS[1]) surface = pygame.Surface((self.location.w, text.get_height()), SRCALPHA) surface.blit(text, (0, 0)) + surface.fill(self.COLORS[1], (0, surface.get_height() - 1, self.location.w, 1)) row.add_frame(surface) row.display_surface = self.get_current_frame() row.location.bottom = 0 self.rows.append(row) + full_path = os.path.join(os.path.sep.join(self.trail), path) + if self.get_audio().is_sound_file(full_path): + button = AudioPanelButton(self, self.preview, {"path": full_path}, [row, self]) + row.set_child("button", button) + frame = pygame.Surface([text.get_height()] * 2, SRCALPHA) + w, h = frame.get_size() + pygame.draw.polygon( + frame, self.COLORS[1], ((w * .25, h * .25), (w * .25, h * .75), (w * .75, h * .5))) + button.add_frame(frame) + button.display_surface = row.get_current_frame() + button.location.right = self.location.w + + def preview(self, path): + if self.get_audio().is_sound_file(path): + if self.previewing_sound is not None: + self.previewing_sound.stop() + self.previewing_sound = SoundEffect(self, path) + self.previewing_sound.play() def update(self): self.get_current_frame().blit(self.background, (0, 0)) @@ -609,9 +741,12 @@ class AudioPanelSpinner(Sprite): class AudioPanelButton(Sprite): - def __init__(self, parent, callback): + def __init__(self, parent, callback, callback_kwargs={}, containers=[]): Sprite.__init__(self, parent) self.callback = callback + self.callback_kwargs = callback_kwargs + self.containers = containers + self.clickable = True self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN) def unsubscribe(self, callback=None, kind=None): @@ -621,5 +756,12 @@ class AudioPanelButton(Sprite): Sprite.unsubscribe(self, callback, kind) def respond(self, event): - if event.button == 1 and self.collide(event.pos): - self.callback() + if self.clickable and event.button == 1: + pos = Vector(*event.pos) + for container in self.containers: + pos.move(-container.location.left, -container.location.top) + if self.collide(pos): + self.callback(**self.callback_kwargs) + + def set_clickable(self, clickable=True): + self.clickable = clickable diff --git a/pgfw/Configuration.py b/pgfw/Configuration.py index 7b59cf5..65a7d51 100644 --- a/pgfw/Configuration.py +++ b/pgfw/Configuration.py @@ -89,6 +89,7 @@ class Configuration(RawConfigParser): section = "input" add_section(section) set_option(section, "release-suffix", "-release", False) + set_option(section, "confirm-quit", "no", False) section = "sprite" add_section(section) set_option(section, "transparent-color", "magenta", False) @@ -124,7 +125,7 @@ class Configuration(RawConfigParser): set_option(section, "volume-up", "K_F2", False) set_option(section, "volume-mute", "K_F3", False) set_option(section, "toggle-interpolator", "K_F7", False) - set_option(section, "toggle-audio-panel", "K_a", False) + set_option(section, "toggle-audio-panel", "K_F6", False) section = "joy" add_section(section) set_option(section, "advance", "7", False) @@ -145,8 +146,10 @@ class Configuration(RawConfigParser): set_option(section, "sfx-default-path", "~/storage/audio/sfx/default", False) set_option(section, "sfx-repository-path", "~/storage/audio/sfx/all", False) set_option(section, "sfx-project-path", "sfx", False) - set_option(section, "sfx-volume", "1.0", False) set_option(section, "sfx-extensions", "wav, ogg, mp3", False) + set_option(section, "bgm-repository-path", "~/storage/audio/bgm", False) + set_option(section, "bgm-project-path", "bgm", False) + set_option(section, "sfx-volume", "1.0", False) set_option(section, "panel-font", None, False) section = "interpolator-gui" add_section(section) @@ -407,6 +410,8 @@ class TypeDeclarations(dict): "int-list": ["dimensions", "framerate-text-color", "framerate-text-background"]}, + "input": {"bool": "confirm-quit"}, + "screen-captures": {"path": ["rel-path", "path"]}, "video-recordings": {"path": ["rel-path", "path"], @@ -443,7 +448,8 @@ class TypeDeclarations(dict): "path": "panel-font", "path-list": [ - "sfx-default-path", "sfx-repository-path", "sfx-project-path" + "sfx-default-path", "sfx-repository-path", "sfx-project-path", + "bgm-repository-path", "bgm-project-path" ], "float": "sfx-volume" diff --git a/pgfw/Delegate.py b/pgfw/Delegate.py index 68640bc..ca516ca 100644 --- a/pgfw/Delegate.py +++ b/pgfw/Delegate.py @@ -1,3 +1,5 @@ +import collections + from pygame.event import get, pump, Event, post from pygame.locals import * @@ -8,7 +10,7 @@ class Delegate(GameChild): def __init__(self, game): GameChild.__init__(self, game) - self.subscribers = dict() + self.subscribers = collections.OrderedDict() self.load_configuration() self.disable() self.cancelling_propagation = False diff --git a/pgfw/Game.py b/pgfw/Game.py index c6eac81..76685c7 100644 --- a/pgfw/Game.py +++ b/pgfw/Game.py @@ -13,12 +13,16 @@ from .Profile import Profile from .VideoRecorder import VideoRecorder from .Interpolator import Interpolator from .TimeFilter import TimeFilter +from .Sprite import Sprite +from .confirm_quit_message import CONFIRM_QUIT_MESSAGE +from .extension import * class Game(Animation): resource_path = None def __init__(self, config_rel_path=None, type_declarations=None): + self.confirming_quit = False self.profile = Profile(self) self.time_filter = TimeFilter(self) Animation.__init__(self, self) @@ -32,8 +36,6 @@ class Game(Animation): os.putenv("SDL_NOMOUSE", "1") pygame.init() self.set_children() - self.subscribe(self.end, QUIT) - self.subscribe(self.end) self.register(self.get_input().unsuppress) self.delegate.enable() @@ -44,6 +46,8 @@ class Game(Animation): def set_children(self): self.delegate = Delegate(self) + self.subscribe(self.end, QUIT) + self.subscribe(self.end) self.display = Display(self) self.mainloop = Mainloop(self) self.input = Input(self) @@ -57,13 +61,37 @@ class Game(Animation): self.delegate.dispatch() if not self.interpolator.is_gui_active(): Animation.update(self) - self.update() + if self.confirming_quit: + self.time_filter.close() + self.draw_confirm_quit_dialog() + else: + self.time_filter.open() + self.update() else: self.interpolator.gui.update() self.audio.update() if self.video_recorder.requested: self.video_recorder.update() + def draw_confirm_quit_dialog(self): + dialog = Sprite(self) + dialog.add_frame(self.get_confirm_quit_surface()) + dialog.location.center = self.get_display_surface().get_rect().center + dialog.update() + + def get_confirm_quit_surface(self): + lines = CONFIRM_QUIT_MESSAGE.strip().split("\n") + w, h = len(lines[0]), len(lines) + text = pygame.Surface((w, h), SRCALPHA) + for y, line in enumerate(lines): + for x, char in enumerate(line.strip()): + if char == "#": + text.set_at((x, y), (0, 0, 0)) + text = pygame.transform.scale2x(text) + text = pygame.transform.scale2x(text) + return get_boxed_surface( + text, background=(255, 255, 255), border=(0, 0, 0), border_width=24, padding=12) + def run(self): self.mainloop.run() @@ -80,7 +108,16 @@ class Game(Animation): self.get_input().suppress() self.play(self.get_input().unsuppress, delay=length, play_once=True) - def end(self, evt): - if evt.type == QUIT or self.delegate.compare(evt, "quit"): - self.mainloop.stop() - self.profile.end() + def end(self, event): + if event.type == QUIT or self.delegate.compare(event, "quit"): + if self.confirming_quit or not self.get_configuration("input", "confirm-quit"): + self.mainloop.stop() + self.profile.end() + else: + print("press quit again to confirm or press any other button to cancel...") + self.confirming_quit = True + elif self.delegate.compare(event, "any"): + if self.confirming_quit and self.delegate.compare(event, "any"): + print("cancelled quit...") + self.confirming_quit = False + self.delegate.cancel_propagation() diff --git a/pgfw/Input.py b/pgfw/Input.py index 8907161..48e4d09 100644 --- a/pgfw/Input.py +++ b/pgfw/Input.py @@ -1,7 +1,7 @@ from time import time as get_secs from pygame import joystick as joy -from pygame.key import get_pressed +from pygame.key import get_pressed, get_mods from pygame.locals import * from .GameChild import * @@ -18,6 +18,7 @@ class Input(GameChild): self.load_configuration() self.set_any_press_ignore_list() self.unsuppress() + self.unsuppress_any_on_mods() self.subscribe_to_events() self.build_key_map() self.build_joy_button_map() @@ -31,12 +32,19 @@ class Input(GameChild): def set_any_press_ignore_list(self): self.any_press_ignored = set(["capture-screen", "toggle-fullscreen", "reset-game", "record-video", "quit", - "mute", "toggle-interpolator"]) + "toggle-interpolator", "toggle-audio-panel", + "volume-down", "volume-up", "volume-mute"]) self.any_press_ignored_keys = set() def unsuppress(self): self.suppressed = False + def unsuppress_any_on_mods(self): + self.suppressed_any_key_on_mods = False + + def suppress_any_key_on_mods(self): + self.suppressed_any_key_on_mods = True + def subscribe_to_events(self): self.subscribe(self.translate_key, KEYDOWN) self.subscribe(self.translate_key, KEYUP) @@ -72,7 +80,9 @@ class Input(GameChild): self.post_command(cmd, cancel=cancel) posted = cmd if (not posted or posted not in self.any_press_ignored) and \ - key not in self.any_press_ignored_keys: + key not in self.any_press_ignored_keys and \ + (not self.suppressed_any_key_on_mods or \ + (not get_mods() & KMOD_CTRL and not get_mods() & KMOD_ALT)): self.post_any_command(key, cancel) def post_command(self, cmd, **attributes): diff --git a/pgfw/Sprite.py b/pgfw/Sprite.py index 7c839b3..2f9ebba 100644 --- a/pgfw/Sprite.py +++ b/pgfw/Sprite.py @@ -25,6 +25,7 @@ class Sprite(Animation): self.neighbors = neighbors self.mass = mass self._step = EVector() + self.children = {} self.set_frameset(0) self.locations.append(Location(self)) self.motion_overflow = Vector() @@ -38,6 +39,15 @@ class Sprite(Animation): return Animation.__getattr__(self, name) raise AttributeError(name) + def add_frameset(self, order=[], framerate=None, name=None, switch=False): + if framerate is None: + framerate = self.accounts[self.default_method].interval + frameset = Frameset(self, order, framerate, name) + self.framesets.append(frameset) + if switch: + self.set_frameset(len(self.framesets) - 1) + return frameset + def set_frameset(self, identifier): if isinstance(identifier, str): for ii, frameset in enumerate(self.framesets): @@ -109,6 +119,10 @@ class Sprite(Animation): if frameset.length() > 1: self.play() + def add_frames(self, frames): + for frame in frames: + self.add_frame(frame) + def shift_frame(self): self.get_current_frameset().shift() @@ -188,13 +202,6 @@ class Sprite(Animation): for location in self.locations: location.fader.set_alpha() - def add_frameset(self, order=[], framerate=None, name=None, switch=False): - frameset = Frameset(self, order, framerate, name) - self.framesets.append(frameset) - if switch: - self.set_frameset(len(self.framesets) - 1) - return frameset - def hide(self): for location in self.locations: location.hide() @@ -232,6 +239,15 @@ class Sprite(Animation): def is_stepping(self): return bool(self._step) + def set_child(self, name, child): + self.children[name] = child + + def get_child(self, name): + return self.children[name] + + def has_child(self, name): + return name in self.children + def update(self, areas=None, substitute=None, flags=0): Animation.update(self) if self.is_stepping(): @@ -241,6 +257,8 @@ class Sprite(Animation): neighbor.mass)) if self.get_current_frameset().length(): self.draw(areas, substitute, flags) + for child in self.children.values(): + child.update() # for location in self.locations: # location.update() diff --git a/pgfw/confirm_quit_message.py b/pgfw/confirm_quit_message.py new file mode 100644 index 0000000..c696381 --- /dev/null +++ b/pgfw/confirm_quit_message.py @@ -0,0 +1,10 @@ +CONFIRM_QUIT_MESSAGE = ''' +............................................................ +..###..###..#....#.####.#.####..#...#....###..#...#.#.#####. +.#....#...#.##...#.#....#.#...#.##.##...#...#.#...#.#...#... +.#....#...#.#.#..#.####.#.#...#.#.#.#...#...#.#...#.#...#... +.#....#...#.#..#.#.#....#.####..#...#...#.#.#.#...#.#...#... +.#....#...#.#...##.#....#.#...#.#...#...#..#..#...#.#...#... +..###..###..#....#.#....#.#...#.#...#....##.#..###..#...#... +............................................................ +''' diff --git a/pgfw/extension.py b/pgfw/extension.py index 6b20354..bef954b 100644 --- a/pgfw/extension.py +++ b/pgfw/extension.py @@ -1,12 +1,20 @@ import itertools, random from math import sin, cos, atan2, radians, sqrt, pi -from pygame import Surface, PixelArray, Color, Rect, draw +from pygame import Surface, PixelArray, Color, Rect, draw, gfxdraw from pygame.mixer import get_num_channels, Channel from pygame.locals import * from .Vector import Vector +def clamp(n, min_n, max_n): + if n < min_n: + return min_n + elif n > max_n: + return max_n + else: + return n + def get_step(start, end, speed): x0, y0 = start x1, y1 = end @@ -352,6 +360,27 @@ def get_lightened_color(color, lightness): h, s, _, a = color.hsla return get_hsla_color(h, s, lightness, a) +def get_glow_frames(radius, segments, colors=[(0, 0, 0), (255, 255, 255)], minsize=4, transparency=True): + frames = [] + radius = int(round(radius)) + sizes = [int(round(minsize + float(ii) / (segments - 1) * (radius - minsize))) for ii in range(segments)] + if transparency: + alpha_step = 255.0 / segments + alpha = alpha_step + else: + alpha = 255 + for color_offset in range(len(colors)): + frame = Surface([radius * 2] * 2, SRCALPHA if transparency else None) + if transparency: + alpha = alpha_step + for segment_ii, segment_radius in enumerate(reversed(sizes)): + color = Color(*(colors[(color_offset + segment_ii) % len(colors)] + (int(round(alpha)),))) + gfxdraw.filled_circle(frame, radius, radius, int(round(segment_radius)), color) + if transparency: + alpha += alpha_step + frames.append(frame) + return frames + # http://www.pygame.org/wiki/BezierCurve def compute_bezier_points(vertices, numPoints=60): points = []