diff --git a/brutalmaze/characters.py b/brutalmaze/characters.py index ddb31aa..7e9312e 100644 --- a/brutalmaze/characters.py +++ b/brutalmaze/characters.py @@ -25,8 +25,8 @@ from sys import modules from .constants import ( TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY, - ENEMY_SPEED, ENEMY_HP, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, AROUND_HERO, - ADJACENTS, EMPTY, SQRT2, ENEMIES) + ENEMY_SPEED, ENEMY_HP, SFX_SPAWN, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, + AROUND_HERO, ADJACENTS, EMPTY, SQRT2, ENEMIES) from .misc import sign, randsign, regpoly, fill_aapolygon, play from .weapons import Bullet @@ -51,7 +51,6 @@ class Hero: spin_queue (float): frames left to finish spinning wound (float): amount of wound wounds (deque of float): wounds in time of an attack (ATTACK_SPEED) - sfx_heart (pygame.mixer.Sound): heart beat sound effect """ def __init__(self, surface, fps, maze_size): self.surface = surface @@ -68,8 +67,6 @@ class Hero: self.spin_queue = self.wound = 0.0 self.wounds = deque([0.0]) - self.sfx_heart = SFX_HEART - def update(self, fps): """Update the hero.""" if self.dead: @@ -87,7 +84,7 @@ class Hero: if self.wound < 0: self.wound = 0.0 self.wounds.append(0.0) if self.next_beat <= 0: - play(self.sfx_heart) + play(SFX_HEART) self.next_beat = MIN_BEAT*(2 - self.wound/HERO_HP) else: self.next_beat -= 1000 / fps @@ -168,7 +165,6 @@ class Enemy: spin_speed (float): speed of spinning (in frames per slash) spin_queue (float): frames left to finish spinning wound (float): amount of wound - sfx_slash (pygame.mixer.Sound): sound effect of slashed hero """ def __init__(self, maze, x, y, color): self.maze = maze @@ -182,8 +178,6 @@ class Enemy: self.spin_speed = self.maze.fps / ENEMY_HP self.spin_queue = self.wound = 0.0 - self.sfx_slash = SFX_SLASH_HERO - @property def pos(self): """Coordinates (in pixels) of the center of the enemy.""" @@ -227,7 +221,7 @@ class Enemy: if self.maze.map[srcx+i//w][srcy+i//u] == WALL: return False self.awake = True self.maze.map[self.x][self.y] = ENEMY - play(self.maze.sfx_spawn, self.spawn_volume, self.get_angle()+pi) + play(SFX_SPAWN, self.x, self.y) return True def fire(self): @@ -317,7 +311,7 @@ class Enemy: if not self.spin_queue and not self.fire() and not self.move(): self.spin_queue = randsign() * self.spin_speed if not self.maze.hero.dead: - play(self.sfx_slash, self.get_slash(), self.get_angle()) + play(SFX_SLASH_HERO, self.x, self.y, self.get_slash()) if round(self.spin_queue) != 0: self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed self.spin_queue -= sign(self.spin_queue) diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py index 1fab35a..396cc2b 100644 --- a/brutalmaze/constants.py +++ b/brutalmaze/constants.py @@ -20,25 +20,23 @@ __doc__ = 'Brutal Maze module for shared constants' from string import ascii_lowercase -from pkg_resources import resource_filename as pkg_file import pygame -from pygame.mixer import Sound +from pkg_resources import resource_filename as pkg_file SETTINGS = pkg_file('brutalmaze', 'settings.ini') ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png')) -NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg') -mixer = pygame.mixer.get_init() -if mixer is None: pygame.mixer.init(frequency=44100) -SFX_SPAWN = Sound(pkg_file('brutalmaze', 'soundfx/spawn.ogg')) -SFX_SLASH_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg')) -SFX_SLASH_HERO = Sound(pkg_file('brutalmaze', 'soundfx/slash-hero.ogg')) -SFX_SHOT_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg')) -SFX_SHOT_HERO = Sound(pkg_file('brutalmaze', 'soundfx/shot-hero.ogg')) -SFX_MISSED = Sound(pkg_file('brutalmaze', 'soundfx/missed.ogg')) -SFX_HEART = Sound(pkg_file('brutalmaze', 'soundfx/heart.ogg')) -SFX_LOSE = Sound(pkg_file('brutalmaze', 'soundfx/lose.ogg')) -if mixer is None: pygame.mixer.quit() +SFX_NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg') +SFX_SPAWN = pkg_file('brutalmaze', 'soundfx/spawn.ogg') +SFX_SLASH_ENEMY = pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg') +SFX_SLASH_HERO = pkg_file('brutalmaze', 'soundfx/slash-hero.ogg') +SFX_SHOT_ENEMY = pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg') +SFX_SHOT_HERO = pkg_file('brutalmaze', 'soundfx/shot-hero.ogg') +SFX_MISSED = pkg_file('brutalmaze', 'soundfx/missed.ogg') +SFX_HEART = pkg_file('brutalmaze', 'soundfx/heart.ogg') +SFX_LOSE = pkg_file('brutalmaze', 'soundfx/lose.ogg') +SFX = (SFX_NOISE, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_SLASH_HERO, + SFX_SHOT_ENEMY, SFX_SHOT_HERO, SFX_MISSED, SFX_HEART, SFX_LOSE) SQRT2 = 2 ** 0.5 INIT_SCORE = 2 diff --git a/brutalmaze/game.py b/brutalmaze/game.py index e5670f8..2b7d331 100644 --- a/brutalmaze/game.py +++ b/brutalmaze/game.py @@ -32,11 +32,12 @@ from threading import Thread with redirect_stdout(StringIO()): import pygame from pygame import KEYDOWN, MOUSEBUTTONUP, QUIT, VIDEORESIZE from pygame.time import Clock, get_ticks +from palace import free, use_context, Device, Context from appdirs import AppDirs -from .constants import SETTINGS, ICON, NOISE, HERO_SPEED, MIDDLE +from .constants import SETTINGS, ICON, SFX, SFX_NOISE, HERO_SPEED, MIDDLE from .maze import Maze -from .misc import sign, deg, join +from .misc import sign, deg, join, play, clean_sources class ConfigReader: @@ -104,17 +105,12 @@ class ConfigReader: class Game: """Object handling main loop and IO.""" - def __init__(self, config): - pygame.mixer.pre_init(frequency=44100) + def __init__(self, config: ConfigReader): pygame.init() self.headless = config.headless and config.server - if config.muted or self.headless: - pygame.mixer.quit() - else: - pygame.mixer.music.load(NOISE) - pygame.mixer.music.set_volume(config.musicvol) - pygame.mixer.music.play(-1) - pygame.display.set_icon(ICON) + if not self.headless: pygame.display.set_icon(ICON) + self.actx = None if self.headless else Context(Device()) + self._mute = config.muted if config.server: self.server = socket() @@ -128,8 +124,7 @@ class Game: else: self.server = self.sockinp = None - # self.fps is a float to make sure floordiv won't be used in Python 2 - self.max_fps, self.fps = config.max_fps, float(config.max_fps) + self.max_fps, self.fps = config.max_fps, config.max_fps self.musicvol = config.musicvol self.touch = config.touch self.key, self.mouse = config.key, config.mouse @@ -138,7 +133,35 @@ class Game: self.hero = self.maze.hero self.clock, self.paused = Clock(), False - def __enter__(self): return self + def __enter__(self): + if self.actx is not None: + use_context(self.actx) + self.actx.listener.position = MIDDLE, 0, MIDDLE + self.actx.listener.gain = not self._mute + play(SFX_NOISE) + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self.server is not None: self.server.close() + if not self.hero.dead: self.maze.dump_records() + if self.actx is not None: + free(SFX) + clean_sources() + use_context(None) + self.actx.destroy() + self.actx.device.close() + pygame.quit() + + @property + def mute(self): + """Mute state.""" + return getattr(self, '_mute', 1) + + @mute.setter + def mute(self, value): + """Mute state.""" + self._mute = int(bool(value)) + self.actx.listener.gain = not self._mute def export_txt(self): """Export maze data to string.""" @@ -161,13 +184,7 @@ class Game: self.maze.resize((event.w, event.h)) elif event.type == KEYDOWN: if event.key == self.key['mute']: - if pygame.mixer.get_init() is None: - pygame.mixer.init(frequency=44100) - pygame.mixer.music.load(NOISE) - pygame.mixer.music.set_volume(self.musicvol) - pygame.mixer.music.play(-1) - else: - pygame.mixer.quit() + self.mute ^= 1 elif not self.server: if event.key == self.key['new']: self.maze.reinit() @@ -196,6 +213,7 @@ class Game: if not self.paused: self.maze.update(self.fps) if not self.headless: self.maze.draw() self.clock.tick(self.fps) + clean_sources() return True def move(self, x=0, y=0): @@ -308,11 +326,6 @@ class Game: slashing = buttons[self.mouse['slash']] self.control(right, down, angle, firing, slashing) - def __exit__(self, exc_type, exc_value, traceback): - if self.server is not None: self.server.close() - if not self.hero.dead: self.maze.dump_records() - pygame.quit() - def main(): """Start game and main loop.""" diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py index f7ba6e7..8be20fe 100644 --- a/brutalmaze/maze.py +++ b/brutalmaze/maze.py @@ -29,7 +29,7 @@ import pygame from .characters import Hero, new_enemy from .constants import ( EMPTY, WALL, HERO, ENEMY, ROAD_WIDTH, WALL_WIDTH, CELL_WIDTH, CELL_NODES, - MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, SQRT2, SFX_SPAWN, + MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, SQRT2, SFX_SPAWN, SFX_MISSED, SFX_SLASH_ENEMY, SFX_LOSE, ADJACENTS, TANGO_VALUES, BG_COLOR, FG_COLOR, COLORS, HERO_HP, ENEMY_HP, ATTACK_SPEED, MAX_WOUND, HERO_SPEED, BULLET_LIFETIME, JSON_SEPARATORS) @@ -63,8 +63,6 @@ class Maze: glitch (float): time that the maze remain flashing colors (in ms) next_slashfx (float): time until next slash effect of the hero (in ms) slashd (float): minimum distance for slashes to be effective - sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy - sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose export (list of defaultdict): records of game states export_dir (str): directory containing records of game states export_rate (float): milliseconds per snapshot @@ -293,7 +291,7 @@ class Maze: if d > 0: wound = d * SQRT2 / self.distance if self.next_slashfx <= 0: - play(self.sfx_slash, wound, enemy.get_angle()) + play(SFX_SLASH_ENEMY, enemy.x, enemy.y, wound) self.next_slashfx = ATTACK_SPEED enemy.hit(wound / self.hero.spin_speed) if enemy.wound >= ENEMY_HP: @@ -323,7 +321,7 @@ class Maze: enemy = new_enemy(self, gridx, gridy) enemy.awake = True self.map[gridx][gridy] = ENEMY - play(self.sfx_spawn, enemy.spawn_volume, enemy.get_angle()) + play(SFX_SPAWN, enemy.x, enemy.y) enemy.hit(wound) self.enemies.append(enemy) continue @@ -334,17 +332,17 @@ class Maze: self.score += enemy.wound enemy.die() self.add_enemy() - play(bullet.sfx_hit, wound, bullet.angle) + play(bullet.sfx_hit, gridx, gridy, wound) fallen.append(i) break elif bullet.get_distance(self.x, self.y) < self.distance: if block: self.hero.next_strike = (abs(self.hero.spin_queue/self.fps) + ATTACK_SPEED) - play(bullet.sfx_missed, wound, bullet.angle + pi) + play(SFX_MISSED, gain=wound) else: self.hit_hero(wound, bullet.color) - play(bullet.sfx_hit, wound, bullet.angle + pi) + play(bullet.sfx_hit, gain=wound) fallen.append(i) for i in reversed(fallen): self.bullets.pop(i) @@ -510,7 +508,7 @@ class Maze: self.destx = self.desty = MIDDLE self.stepx = self.stepy = 0 self.vx = self.vy = 0.0 - play(self.sfx_lose) + play(SFX_LOSE) self.dump_records() def reinit(self): diff --git a/brutalmaze/misc.py b/brutalmaze/misc.py index 94a2c39..92000a4 100644 --- a/brutalmaze/misc.py +++ b/brutalmaze/misc.py @@ -26,8 +26,9 @@ from random import shuffle import pygame from pygame.gfxdraw import filled_polygon, aapolygon +from palace import Buffer, Source -from .constants import ADJACENTS, CORNERS +from .constants import ADJACENTS, CORNERS, MIDDLE def randsign(): @@ -81,29 +82,34 @@ def around(x, y): return chain(a, c) -def play(sound, volume=1.0, angle=None): - """Play a pygame.mixer.Sound at the given volume.""" - if pygame.mixer.get_init() is None: return - if pygame.mixer.find_channel() is None: - pygame.mixer.set_num_channels(pygame.mixer.get_num_channels() + 1) - - channel = sound.play() - if angle is None: - channel.set_volume(volume) - else: - delta = cos(angle) - volumes = [volume * (1-delta), volume * (1+delta)] - for i, v in enumerate(volumes): - if v > 1: - volumes[i - 1] += v - 1 - volumes[i] = 1.0 - sound.set_volume(1.0) - channel.set_volume(*volumes) - - def json_rec(directory): """Return path to JSON file to be created inside the given directory based on current time local to timezone in ISO 8601 format. """ return path.join( directory, '{}.json'.format(datetime.now().isoformat()[:19])) + + +def play(sound: str, x: float = MIDDLE, y: float = MIDDLE, + gain: float = 1.0) -> None: + """Play a sound at the given position.""" + buffer = Buffer(sound) + source = buffer.play() + source.spatialize = True + source.position = x, 0, y + source.gain = gain + sources.append(source) + + +def clean_sources() -> None: + """Destroyed stopped sources.""" + global sources + sources, tmp = [], sources + for source in tmp: + if source.playing: + sources.append(source) + else: + source.destroy() + + +sources = [] diff --git a/brutalmaze/weapons.py b/brutalmaze/weapons.py index a149841..b66ad80 100644 --- a/brutalmaze/weapons.py +++ b/brutalmaze/weapons.py @@ -21,7 +21,7 @@ __doc__ = 'Brutal Maze module for weapon classes' from math import cos, sin from .constants import (BULLET_LIFETIME, SFX_SHOT_ENEMY, SFX_SHOT_HERO, - SFX_MISSED, BULLET_SPEED, ENEMY_HP, TANGO, BG_COLOR) + BULLET_SPEED, ENEMY_HP, TANGO, BG_COLOR) from .misc import regpoly, fill_aapolygon @@ -34,8 +34,7 @@ class Bullet: angle (float): angle of the direction the bullet pointing (in radians) color (str): bullet's color name fall_time (int): time until the bullet fall down - sfx_hit (pygame.mixer.Sound): sound effect indicating target was hit - sfx_missed (pygame.mixer.Sound): sound effect indicating a miss shot + sfx_hit (str): sound effect indicating target was hit """ def __init__(self, surface, x, y, angle, color): self.surface = surface @@ -45,7 +44,6 @@ class Bullet: self.sfx_hit = SFX_SHOT_ENEMY else: self.sfx_hit = SFX_SHOT_HERO - self.sfx_missed = SFX_MISSED def update(self, fps, distance): """Update the bullet.""" diff --git a/pyproject.toml b/pyproject.toml index bed1ac4..8cbcad4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ module = 'brutalmaze' author = 'Nguyễn Gia Phong' author-email = 'mcsinyx@disroot.org' home-page = 'https://github.com/McSinyx/brutalmaze' -requires = ['appdirs', 'pygame>=1.9', 'setuptools'] +requires = ['appdirs', 'palace', 'pygame>=1.9', 'setuptools'] description-file = 'README.rst' classifiers = [ 'Development Status :: 4 - Beta', @@ -28,5 +28,4 @@ license = 'AGPLv3+' brutalmaze = "brutalmaze.game:main" [tool.flit.sdist] -include = ['wiki'] exclude = ['docs']