summary refs log tree commit diff homepage
diff options
context:
space:
mode:
authorNguyễn Gia Phong <vn.mcsinyx@gmail.com>2018-02-19 16:55:55 +0700
committerNguyễn Gia Phong <vn.mcsinyx@gmail.com>2018-02-19 16:55:55 +0700
commitbc47fb3f304fba062638d365ff96ca395a0c9d33 (patch)
tree43fbf3f8c129cb5c188db3a9d93e9dd452478d98
parent8852a9f67886ccdcaf9083cf7323c7290a4dce57 (diff)
downloadbrutalmaze-0.5.3.tar.gz
Add sound options and semi-separate front-end from engine 0.5.3
-rw-r--r--brutalmaze/characters.py17
-rw-r--r--brutalmaze/constants.py30
-rw-r--r--brutalmaze/main.py54
-rw-r--r--brutalmaze/maze.py42
-rw-r--r--brutalmaze/misc.py1
-rw-r--r--brutalmaze/settings.ini8
-rw-r--r--brutalmaze/weapons.py20
m---------wiki0
8 files changed, 106 insertions, 66 deletions
diff --git a/brutalmaze/characters.py b/brutalmaze/characters.py
index faa67c4..6201569 100644
--- a/brutalmaze/characters.py
+++ b/brutalmaze/characters.py
@@ -24,7 +24,6 @@ from random import choice, randrange, shuffle
 from sys import modules
 
 import pygame
-from pygame.mixer import Sound
 from pygame.time import get_ticks
 
 from .constants import *
@@ -50,7 +49,7 @@ class Hero:
         spin_speed (float): speed of spinning (in frames per slash)
         spin_queue (float): frames left to finish spinning
         wound (float): amount of wound
-        sfx_heart (Sound): heart beat sound effect
+        sfx_heart (pygame.mixer.Sound): heart beat sound effect
     """
     def __init__(self, surface, fps):
         self.surface = surface
@@ -64,7 +63,7 @@ class Hero:
         self.spin_speed = fps / HERO_HP
         self.spin_queue = self.wound = 0.0
 
-        self.sfx_heart = Sound(SFX_HEART)
+        self.sfx_heart = SFX_HEART
 
     def update(self, fps):
         """Update the hero."""
@@ -92,6 +91,9 @@ class Hero:
             x, y = pygame.mouse.get_pos()
             self.angle = atan2(y - self.y, x - self.x)
             self.spin_queue = 0.0
+
+    def draw(self):
+        """Draw the hero."""
         trigon = regpoly(3, self.R, self.angle, self.x, self.y)
         fill_aapolygon(self.surface, trigon, self.color[int(self.wound)])
 
@@ -117,7 +119,7 @@ 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 (Sound): sound effect indicating close-range attack damage
+        sfx_slash (pygame.mixer.Sound): sound effect of slashed hero
     """
     def __init__(self, maze, x, y, color):
         self.maze = maze
@@ -132,7 +134,7 @@ class Enemy:
         self.spin_speed = self.maze.fps / ENEMY_HP
         self.spin_queue = self.wound = 0.0
 
-        self.sfx_slash = Sound(SFX_SLASH_HERO)
+        self.sfx_slash = SFX_SLASH_HERO
 
     def get_pos(self):
         """Return coordinate of the center of the enemy."""
@@ -238,6 +240,7 @@ class Enemy:
 
     def draw(self):
         """Draw the enemy."""
+        if get_ticks() < self.maze.next_move and not self.awake: return
         radious = self.maze.distance/SQRT2 - self.awake*2
         square = regpoly(4, radious, self.angle, *self.get_pos())
         color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR
@@ -257,7 +260,6 @@ class Enemy:
                 self.spin_queue -= sign(self.spin_queue)
             else:
                 self.angle, self.spin_queue = pi / 4, 0.0
-        if self.awake or get_ticks() >= self.maze.next_move: self.draw()
 
     def hit(self, wound):
         """Handle the enemy when it's attacked."""
@@ -290,8 +292,7 @@ class Chameleon(Enemy):
 
     def draw(self):
         """Draw the Chameleon."""
-        if (not self.awake or self.spin_queue
-            or get_ticks() < max(self.visible, self.maze.next_move)):
+        if not self.awake or get_ticks() < self.visible or self.spin_queue:
             Enemy.draw(self)
 
     def hit(self, wound):
diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py
index 0792808..3bd34b1 100644
--- a/brutalmaze/constants.py
+++ b/brutalmaze/constants.py
@@ -19,21 +19,25 @@
 
 __doc__ = 'brutalmaze module for shared constants'
 
-from pkg_resources import resource_filename
-from pygame import image
+from pkg_resources import resource_filename as pkg_file
+import pygame
 from pygame.mixer import Sound
 
-SETTINGS = resource_filename('brutalmaze', 'settings.ini')
-ICON = image.load(resource_filename('brutalmaze', 'icon.png'))
-MUSIC = resource_filename('brutalmaze', 'soundfx/music.ogg')
-SFX_SPAWN = resource_filename('brutalmaze', 'soundfx/spawn.ogg')
-SFX_SLASH_ENEMY = resource_filename('brutalmaze', 'soundfx/slash-enemy.ogg')
-SFX_SLASH_HERO = resource_filename('brutalmaze', 'soundfx/slash-hero.ogg')
-SFX_SHOT_ENEMY = resource_filename('brutalmaze', 'soundfx/shot-enemy.ogg')
-SFX_SHOT_HERO = resource_filename('brutalmaze', 'soundfx/shot-hero.ogg')
-SFX_MISSED = resource_filename('brutalmaze', 'soundfx/missed.ogg')
-SFX_HEART = resource_filename('brutalmaze', 'soundfx/heart.ogg')
-SFX_LOSE = resource_filename('brutalmaze', 'soundfx/lose.ogg')
+SETTINGS = pkg_file('brutalmaze', 'settings.ini')
+ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png'))
+MUSIC = pkg_file('brutalmaze', 'soundfx/music.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()
 
 SQRT2 = 2 ** 0.5
 INIT_SCORE = 5**0.5/2 + 0.5     # golden mean
diff --git a/brutalmaze/main.py b/brutalmaze/main.py
index c84b92f..39dca3b 100644
--- a/brutalmaze/main.py
+++ b/brutalmaze/main.py
@@ -35,7 +35,7 @@ from pygame import DOUBLEBUF, KEYDOWN, OPENGL, QUIT, RESIZABLE, VIDEORESIZE
 from pygame.time import Clock, get_ticks
 from appdirs import AppDirs
 
-from .constants import *
+from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED
 from .maze import Maze
 from .misc import sign
 
@@ -44,7 +44,8 @@ class ConfigReader:
     """Object reading and processing INI configuration file for
     Brutal Maze.
     """
-    CONTROL_ALIASES = (('New game', 'new'), ('Pause', 'pause'),
+    CONTROL_ALIASES = (('New game', 'new'), ('Toggle pause', 'pause'),
+                       ('Toggle mute', 'mute'),
                        ('Move left', 'left'), ('Move right', 'right'),
                        ('Move up', 'up'), ('Move down', 'down'),
                        ('Long-range attack', 'shot'),
@@ -57,12 +58,14 @@ class ConfigReader:
         self.config.read(SETTINGS)  # default configuration
         self.config.read(filenames)
 
-    def parse_graphics(self):
-        """Parse graphics configurations."""
+    def parse_output(self):
+        """Parse graphics and sound configurations."""
         self.size = (self.config.getint('Graphics', 'Screen width'),
                      self.config.getint('Graphics', 'Screen height'))
         self.opengl = self.config.getboolean('Graphics', 'OpenGL')
         self.max_fps = self.config.getint('Graphics', 'Maximum FPS')
+        self.muted = self.config.getboolean('Sound', 'Muted')
+        self.musicvol = self.config.getfloat('Sound', 'Music volume')
 
     def parse_control(self):
         """Parse control configurations."""
@@ -84,27 +87,31 @@ class ConfigReader:
 
     def read_args(self, arguments):
         """Read and parse a ArgumentParser.Namespace."""
-        if arguments.size is not None: self.size = arguments.size
-        if arguments.opengl is not None: self.opengl = arguments.opengl
-        if arguments.max_fps is not None: self.max_fps = arguments.max_fps
+        for option in 'size', 'opengl', 'max_fps', 'muted', 'musicvol':
+            value = getattr(arguments, option)
+            if value is not None: setattr(self, option, value)
 
 
 class Game:
     """Object handling main loop and IO."""
-    def __init__(self, size, scrtype, max_fps, key, mouse):
+    def __init__(self, size, scrtype, max_fps, muted, musicvol, key, mouse):
         pygame.mixer.pre_init(frequency=44100)
         pygame.init()
-        pygame.mixer.music.load(MUSIC)
-        pygame.mixer.music.play(-1)
+        if muted:
+            pygame.mixer.quit()
+        else:
+            pygame.mixer.music.load(MUSIC)
+            pygame.mixer.music.set_volume(musicvol)
+            pygame.mixer.music.play(-1)
         pygame.display.set_icon(ICON)
         pygame.fastevent.init()
-        self.clock = Clock()
         # self.fps is a float to make sure floordiv won't be used in Python 2
         self.max_fps, self.fps = max_fps, float(max_fps)
+        self.musicvol = musicvol
         self.key, self.mouse = key, mouse
         self.maze = Maze(max_fps, size, scrtype)
         self.hero = self.maze.hero
-        self.paused = False
+        self.clock, self.paused = Clock(), False
 
     def __enter__(self): return self
 
@@ -145,6 +152,14 @@ class Game:
                     self.maze.__init__(self.fps)
                 elif event.key == self.key['pause'] and not self.hero.dead:
                     self.paused ^= True
+                elif event.key == self.key['mute']:
+                    if pygame.mixer.get_init() is None:
+                        pygame.mixer.init(frequency=44100)
+                        pygame.mixer.music.load(MUSIC)
+                        pygame.mixer.music.set_volume(self.musicvol)
+                        pygame.mixer.music.play(-1)
+                    else:
+                        pygame.mixer.quit()
 
         if not self.hero.dead:
             keys = pygame.key.get_pressed()
@@ -181,7 +196,7 @@ def main():
     parents.append(dirs.user_config_dir)
     filenames = [join(parent, 'settings.ini') for parent in parents]
     config = ConfigReader(filenames)
-    config.parse_graphics()
+    config.parse_output()
 
     # Parse command-line arguments
     parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
@@ -206,6 +221,14 @@ def main():
     parser.add_argument(
         '-f', '--max-fps', type=int, metavar='FPS',
         help='the desired maximum FPS (fallback: {})'.format(config.max_fps))
+    parser.add_argument(
+        '--mute', '-m', action='store_true', default=None,
+        help='mute all sounds (fallback: {})'.format(config.muted))
+    parser.add_argument('--unmute', action='store_false', dest='muted',
+                        help='unmute sound')
+    parser.add_argument(
+        '--music-volume', type=float, metavar='VOL', dest='musicvol',
+        help='between 0.0 and 1.0 (fallback: {})'.format(config.musicvol))
     args = parser.parse_args()
     if args.defaultcfg is not None:
         with open(SETTINGS) as settings: args.defaultcfg.write(settings.read())
@@ -215,10 +238,11 @@ def main():
     # Manipulate config
     if args.config: config.config.read(args.config)
     config.read_args(args)
-    config.parse_graphics()
+    config.parse_output()
     config.parse_control()
 
     # Main loop
     scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
-    with Game(config.size, scrtype, config.max_fps, config.key, config.mouse) as game:
+    with Game(config.size, scrtype, config.max_fps, config.muted,
+              config.musicvol, config.key, config.mouse) as game:
         while game.loop(): pass
diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py
index 0f1f5de..66c4c81 100644
--- a/brutalmaze/maze.py
+++ b/brutalmaze/maze.py
@@ -25,7 +25,6 @@ from random import choice, getrandbits, uniform
 
 import pygame
 from pygame import RESIZABLE
-from pygame.mixer import Sound
 from pygame.time import get_ticks
 
 from .characters import Hero, new_enemy
@@ -76,9 +75,8 @@ class Maze:
         next_move (int): the tick that the hero gets mobilized
         next_slashfx (int): the tick to play next slash effect of the hero
         slashd (float): minimum distance for slashes to be effective
-        sfx_slash (Sound): sound effect indicating an enemy get slashed
-        sfx_shot (Sound): sound effect indicating an enemy get shot
-        sfx_lose (Sound): sound effect to be played when you lose
+        sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy
+        sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose
     """
     def __init__(self, fps, size=None, scrtype=None):
         self.fps = fps
@@ -109,10 +107,9 @@ class Maze:
         self.next_move = self.next_slashfx = 0
         self.slashd = self.hero.R + self.distance/SQRT2
 
-        self.sfx_spawn = Sound(SFX_SPAWN)
-        self.sfx_slash = Sound(SFX_SLASH_ENEMY)
-        self.sfx_shot = Sound(SFX_SHOT_ENEMY)
-        self.sfx_lose = Sound(SFX_LOSE)
+        self.sfx_spawn = SFX_SPAWN
+        self.sfx_slash = SFX_SLASH_ENEMY
+        self.sfx_lose = SFX_LOSE
 
     def add_enemy(self):
         """Add enough enemies."""
@@ -140,13 +137,21 @@ class Maze:
     def draw(self):
         """Draw the maze."""
         self.surface.fill(BG_COLOR)
-        if get_ticks() < self.next_move: return
-        for i in self.rangex:
-            for j in self.rangey:
-                if self.map[i][j] != WALL: continue
-                x, y = self.get_pos(i, j)
-                square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
-                fill_aapolygon(self.surface, square, FG_COLOR)
+        if get_ticks() >= self.next_move:
+            for i in self.rangex:
+                for j in self.rangey:
+                    if self.map[i][j] != WALL: continue
+                    x, y = self.get_pos(i, j)
+                    square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
+                    fill_aapolygon(self.surface, square, FG_COLOR)
+
+        for enemy in self.enemies: enemy.draw()
+        self.hero.draw()
+        bullet_radius = self.distance / 4
+        for bullet in self.bullets: bullet.draw(bullet_radius)
+        pygame.display.flip()
+        pygame.display.set_caption('Brutal Maze - Score: {}'.format(
+            int(self.score - INIT_SCORE)))
 
     def rotate(self):
         """Rotate the maze if needed."""
@@ -266,7 +271,7 @@ class Maze:
                             self.score += enemy.wound
                             enemy.die()
                             self.enemies.pop(j)
-                        play(self.sfx_shot, wound, bullet.angle)
+                        play(bullet.sfx_hit, wound, bullet.angle)
                         fallen.append(i)
                         break
             elif bullet.get_distance(self.x, self.y) < self.distance:
@@ -310,15 +315,12 @@ class Maze:
             for enemy in self.enemies: enemy.wake()
             for bullet in self.bullets: bullet.place(dx, dy)
 
-        self.draw()
         for enemy in self.enemies: enemy.update()
         if not self.hero.dead:
             self.hero.update(fps)
             self.slash()
             self.track_bullets()
-        pygame.display.flip()
-        pygame.display.set_caption('Brutal Maze - Score: {}'.format(
-            int(self.score - INIT_SCORE)))
+        self.draw()
 
     def resize(self, size):
         """Resize the maze."""
diff --git a/brutalmaze/misc.py b/brutalmaze/misc.py
index ef04502..af5a736 100644
--- a/brutalmaze/misc.py
+++ b/brutalmaze/misc.py
@@ -76,6 +76,7 @@ def choices(d):
 
 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)
 
diff --git a/brutalmaze/settings.ini b/brutalmaze/settings.ini
index 00a071f..2baeedd 100644
--- a/brutalmaze/settings.ini
+++ b/brutalmaze/settings.ini
@@ -6,6 +6,11 @@ OpenGL: no
 # FPS should not be greater than refresh rate.
 Maximum FPS: 60
 
+[Sound]
+Muted: no
+# Volume must be between 0.0 and 1.0
+Music volume: 1.0
+
 [Control]
 # Input values should be either from Mouse1 to Mouse3 or a keyboard key
 # and they are case-insensitively read.
@@ -13,7 +18,8 @@ Maximum FPS: 60
 # http://www.pygame.org/docs/ref/key.html
 # Key combinations are not supported.
 New game: F2
-Pause: p
+Toggle pause: p
+Toggle mute: m
 Move left: Left
 Move right: Right
 Move up: Up
diff --git a/brutalmaze/weapons.py b/brutalmaze/weapons.py
index c0c860a..3979d30 100644
--- a/brutalmaze/weapons.py
+++ b/brutalmaze/weapons.py
@@ -22,7 +22,6 @@ __doc__ = 'brutalmaze module for weapon classes'
 from math import cos, sin
 
 from pygame.time import get_ticks
-from pygame.mixer import Sound
 
 from .constants import *
 from .misc import regpoly, fill_aapolygon
@@ -37,25 +36,28 @@ class Bullet:
         angle (float): angle of the direction the bullet pointing (in radians)
         color (str): bullet's color name
         fall_time (int): the tick that the bullet will fall down
-        sfx_hit (Sound): sound effect indicating the bullet hits the target
-        sfx_missed (Sound): sound effect indicating the bullet hits the target
+        sfx_hit (pygame.mixer.Sound): sound effect indicating target was hit
+        sfx_missed (pygame.mixer.Sound): sound effect indicating a miss shot
     """
     def __init__(self, surface, x, y, angle, color):
         self.surface = surface
         self.x, self.y, self.angle, self.color = x, y, angle, color
         self.fall_time = get_ticks() + BULLET_LIFETIME
-        # Sound effects of bullets shot by hero are stored in Maze to avoid
-        # unnecessary duplication
-        if color != 'Aluminium':
-            self.sfx_hit = Sound(SFX_SHOT_HERO)
-            self.sfx_missed = Sound(SFX_MISSED)
+        if color == 'Aluminium':
+            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."""
         s = distance * BULLET_SPEED / fps
         self.x += s * cos(self.angle)
         self.y += s * sin(self.angle)
-        pentagon = regpoly(5, distance // 4, self.angle, self.x, self.y)
+
+    def draw(self, radius):
+        """Draw the bullet."""
+        pentagon = regpoly(5, radius, self.angle, self.x, self.y)
         value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
         try:
             fill_aapolygon(self.surface, pentagon, TANGO[self.color][value])
diff --git a/wiki b/wiki
-Subproject 3b014564eb185bb525a59378fd2d9ede65bf0d3
+Subproject 34af1cf8b3e3ea8272d6793a794484a239794d5