Retain game state after pauses

This commit is contained in:
Nguyễn Gia Phong 2018-03-06 21:01:27 +07:00
parent f7c600934e
commit b5039285d5
7 changed files with 71 additions and 65 deletions

View File

@ -23,8 +23,6 @@ from math import atan, atan2, sin, pi
from random import choice, randrange, shuffle
from sys import modules
from pygame.time import get_ticks
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,
@ -42,9 +40,9 @@ class Hero:
angle (float): angle of the direction the hero pointing (in radians)
color (tuple of pygame.Color): colors of the hero on different HPs
R (int): circumradius of the regular triangle representing the hero
next_heal (int): the tick that the hero gains back healing ability
next_beat (int): the tick to play next heart beat
next_strike (int): the tick that the hero can do the next attack
next_heal (float): ms until the hero gains back healing ability
next_beat (float): time until next heart beat (in ms)
next_strike (float): time until the hero can do the next attack (in ms)
slashing (bool): flag indicates if the hero is doing close-range attack
firing (bool): flag indicates if the hero is doing long-range attack
dead (bool): flag indicates if the hero is dead
@ -60,7 +58,7 @@ class Hero:
self.angle, self.color = -pi * 3 / 4, TANGO['Aluminium']
self.R = (w * h / sin(pi*2/3) / 624) ** 0.5
self.next_heal = self.next_beat = self.next_strike = 0
self.next_heal = self.next_beat = self.next_strike = 0.0
self.slashing = self.firing = self.dead = False
self.spin_speed = fps / HERO_HP
self.spin_queue = self.wound = 0.0
@ -72,19 +70,24 @@ class Hero:
if self.dead:
self.spin_queue = 0.0
return
old_speed, time = self.spin_speed, get_ticks()
old_speed = self.spin_speed
self.spin_speed = fps / (HERO_HP-self.wound**0.5)
self.spin_queue *= self.spin_speed / old_speed
if time >= self.next_heal:
if self.next_heal <= 0:
self.wound -= HEAL_SPEED / self.spin_speed / HERO_HP
if self.wound < 0: self.wound = 0.0
if time >= self.next_beat:
else:
self.next_heal -= 1000.0 / fps
if self.next_beat <= 0:
play(self.sfx_heart)
self.next_beat = time + MIN_BEAT*(2 - self.wound/HERO_HP)
self.next_beat = MIN_BEAT*(2 - self.wound/HERO_HP)
else:
self.next_beat -= 1000.0 / fps
self.next_strike -= 1000.0 / fps
full_spin = pi * 2 / self.get_sides()
if self.slashing and time >= self.next_strike:
self.next_strike = time + ATTACK_SPEED
if self.slashing and self.next_strike <= 0:
self.next_strike = ATTACK_SPEED
self.spin_queue = randsign() * self.spin_speed
self.angle -= sign(self.spin_queue) * full_spin
if abs(self.spin_queue) > 0.5:
@ -97,7 +100,7 @@ class Hero:
"""Return the number of sides the hero has. While the hero is
generally a trigon, Agent Orange may turn him into a square.
"""
return 3 if get_ticks() >= self.next_heal else 4
return 3 if self.next_heal <= 0 else 4
def update_angle(self, angle):
"""Turn to the given angle if the hero is not busy slashing."""
@ -106,7 +109,7 @@ class Hero:
unit = pi * 2 / self.get_sides() / self.spin_speed
if abs(delta) < unit:
self.angle, self.spin_queue = angle, 0.0
elif get_ticks() >= self.next_strike:
elif self.next_strike <= 0:
self.spin_queue = delta / unit
def get_color(self):
@ -134,7 +137,7 @@ class Enemy:
angle (float): angle of the direction the enemy pointing (in radians)
color (str): enemy's color name
awake (bool): flag indicates if the enemy is active
next_strike (int): the tick that the enemy can do the next attack
next_strike (float): time until the enemy's next action (in ms)
move_speed (float): speed of movement (in frames per grid)
offsetx, offsety (integer): steps moved from the center of the grid
spin_speed (float): speed of spinning (in frames per slash)
@ -149,7 +152,7 @@ class Enemy:
self.angle, self.color = pi / 4, color
self.awake = False
self.next_strike = 0
self.next_strike = 0.0
self.move_speed = self.maze.fps / ENEMY_SPEED
self.offsetx = self.offsety = 0
self.spin_speed = self.maze.fps / ENEMY_HP
@ -207,11 +210,11 @@ class Enemy:
if self.maze.hero.dead: return False
x, y = self.get_pos()
if (self.maze.get_distance(x, y) > FIRANGE*self.maze.distance
or get_ticks() < self.next_strike
or self.next_strike > 0
or (self.x, self.y) in AROUND_HERO or self.offsetx or self.offsety
or randrange((self.maze.hero.slashing+self.maze.isfast()+1) * 3)):
return False
self.next_strike = get_ticks() + ATTACK_SPEED
self.next_strike = ATTACK_SPEED
self.maze.bullets.append(
Bullet(self.maze.surface, x, y, self.get_angle() + pi, self.color))
return True
@ -224,7 +227,7 @@ class Enemy:
if self.offsety:
self.offsety -= sign(self.offsety)
return True
if get_ticks() < self.next_strike: return False
if self.next_strike > 0: return False
self.move_speed = self.maze.fps / speed
directions = [(sign(MIDDLE - self.x), 0), (0, sign(MIDDLE - self.y))]
@ -267,7 +270,7 @@ class Enemy:
def draw(self):
"""Draw the enemy."""
if get_ticks() < self.maze.next_move and not self.awake: return
if self.maze.next_move > 0 and not self.awake: return
radius = self.maze.distance/SQRT2 - self.awake*2
square = regpoly(4, radius, self.angle, *self.get_pos())
fill_aapolygon(self.maze.surface, square, self.get_color())
@ -277,6 +280,7 @@ class Enemy:
if self.awake:
self.spin_speed, tmp = self.maze.fps / ENEMY_HP, self.spin_speed
self.spin_queue *= self.spin_speed / tmp
self.next_strike -= 1000.0 / self.maze.fps
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:
@ -305,25 +309,30 @@ class Chameleon(Enemy):
"""Object representing an enemy of Chameleon.
Additional attributes:
visible (int): the tick until which the Chameleon is visible
visible (float): time until the Chameleon is visible (in ms)
"""
def __init__(self, maze, x, y):
Enemy.__init__(self, maze, x, y, 'Chameleon')
self.visible = 0
self.visible = 0.0
def wake(self):
"""Wake the Chameleon up if it can see the hero."""
if Enemy.wake(self) is True:
self.visible = get_ticks() + 1000//ENEMY_SPEED
self.visible = 1000.0 / ENEMY_SPEED
def draw(self):
"""Draw the Chameleon."""
if not self.awake or get_ticks() < self.visible or self.spin_queue:
if not self.awake or self.visible > 0 or self.spin_queue:
Enemy.draw(self)
def update(self):
"""Update the Chameleon."""
Enemy.update(self)
if self.awake: self.visible -= 1000.0 / self.maze.fps
def hit(self, wound):
"""Handle the Chameleon when it's attacked."""
self.visible = get_ticks() + 1000//ENEMY_SPEED
self.visible = 1000.0 / ENEMY_SPEED
Enemy.hit(self, wound)

View File

@ -52,7 +52,7 @@ HEAL_SPEED = 1 # HP/s
HERO_SPEED = 5 # grid/s
ENEMY_SPEED = 6 # grid/s
BULLET_SPEED = 15 # grid/s
ATTACK_SPEED = 333 # ms/strike
ATTACK_SPEED = 333.333 # ms/strike
FIRANGE = 6 # grids
BULLET_LIFETIME = 1000.0 * FIRANGE / (BULLET_SPEED-HERO_SPEED) # ms
EMPTY, WALL, HERO, ENEMY = range(4)

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__version__ = '0.5.6'
__version__ = '0.6.0'
import re
from argparse import ArgumentParser, FileType, RawTextHelpFormatter
@ -26,13 +26,12 @@ try: # Python 3
from configparser import ConfigParser
except ImportError: # Python 2
from ConfigParser import ConfigParser
from math import atan2, degrees, radians, pi
from math import atan2, radians, pi
from os.path import join, pathsep
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from sys import stdout
from threading import Thread
import pygame
from pygame import DOUBLEBUF, KEYDOWN, OPENGL, QUIT, RESIZABLE, VIDEORESIZE
from pygame.time import Clock, get_ticks
@ -147,21 +146,21 @@ class Game:
def export(self):
"""Export maze data to a bytes object."""
maze, hero, time = self.maze, self.hero, get_ticks()
maze, hero, = self.maze, self.hero
lines = deque(['{0} {4} {5} {1} {2:d} {3:d}'.format(
COLORS[hero.get_color()], deg(self.hero.angle),
hero.next_strike <= time, hero.next_heal <= time,
hero.next_strike <= 0, hero.next_heal <= 0,
*self.expos(maze.x, maze.y))])
walls = [[1 if maze.map[x][y] == WALL else 0 for x in maze.rangex]
for y in maze.rangey] if maze.next_move <= time else []
for y in maze.rangey] if maze.next_move <= 0 else []
ne = nb = 0
for enemy in maze.enemies:
if not enemy.awake and walls:
walls[enemy.y-maze.rangey[0]][enemy.x-maze.rangex[0]] = WALL
continue
elif enemy.color == 'Chameleon' and maze.next_move <= time:
elif enemy.color == 'Chameleon' and maze.next_move <= 0:
continue
lines.append('{0} {2} {3} {1:.0f}'.format(
COLORS[enemy.get_color()], deg(enemy.angle),
@ -212,20 +211,18 @@ class Game:
self.fps -= 1
elif self.fps < self.max_fps and not self.paused:
self.fps += 5
if not self.paused:
self.maze.update(self.fps)
if not self.headless: self.maze.draw()
if not self.paused: self.maze.update(self.fps)
if not self.headless: self.maze.draw()
self.clock.tick(self.fps)
return True
def move(self, x, y):
"""Command the hero to move faster in the given direction."""
x, y = -x, -y # or move the maze in the reverse direction
stunned = pygame.time.get_ticks() < self.maze.next_move
velocity = self.maze.distance * HERO_SPEED / self.fps
accel = velocity * HERO_SPEED / self.fps
if stunned or not x:
if self.maze.next_move > 0 or not x:
self.maze.vx -= sign(self.maze.vx) * accel
if abs(self.maze.vx) < accel * 2: self.maze.vx = 0.0
elif x * self.maze.vx < 0:
@ -234,7 +231,7 @@ class Game:
self.maze.vx += x * accel
if abs(self.maze.vx) > velocity: self.maze.vx = x * velocity
if stunned or not y:
if self.maze.next_move > 0 or not y:
self.maze.vy -= sign(self.maze.vy) * accel
if abs(self.maze.vy) < accel * 2: self.maze.vy = 0.0
elif y * self.maze.vy < 0:

View File

@ -24,7 +24,6 @@ from math import pi, log
from random import choice, getrandbits, uniform
import pygame
from pygame.time import get_ticks
from .characters import Hero, new_enemy
from .constants import (
@ -75,8 +74,8 @@ class Maze:
enemy_weights (dict): probabilities of enemies to be created
enemies (list of Enemy): alive enemies
hero (Hero): the hero
next_move (int): the tick that the hero gets mobilized
next_slashfx (int): the tick to play next slash effect of the hero
next_move (float): time until the hero gets mobilized (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
@ -107,7 +106,7 @@ class Maze:
self.add_enemy()
self.hero = Hero(self.surface, fps, size)
self.map[MIDDLE][MIDDLE] = HERO
self.next_move = self.next_slashfx = 0
self.next_move = self.next_slashfx = 0.0
self.slashd = self.hero.R + self.distance/SQRT2
self.sfx_spawn = SFX_SPAWN
@ -144,7 +143,7 @@ class Maze:
def draw(self):
"""Draw the maze."""
self.surface.fill(BG_COLOR)
if get_ticks() >= self.next_move:
if self.next_move <= 0:
for i in self.rangex:
for j in self.rangey:
if self.map[i][j] != WALL: continue
@ -217,13 +216,12 @@ class Maze:
"""Handle the hero when he loses HP."""
fx = (uniform(0, sum(self.enemy_weights.values()))
< self.enemy_weights[color])
time = get_ticks()
if (color == 'Butter' or color == 'ScarletRed') and fx:
self.hero.wound += wound * 2.5
elif color == 'Orange' and fx:
self.hero.next_heal = max(self.hero.next_heal, time) + wound*1000
self.hero.next_heal = max(self.hero.next_heal, 0) + wound*1000
elif color == 'SkyBlue' and fx:
self.next_move = max(self.next_move, time) + wound*1000
self.next_move = max(self.next_move, 0) + wound*1000
else:
self.hero.wound += wound
if self.enemy_weights[color] + wound < MAXW:
@ -238,10 +236,10 @@ class Maze:
for i, enemy in enumerate(self.enemies):
d = self.slashd - enemy.get_distance()
if d > 0:
wound, time = d * SQRT2 / self.distance, get_ticks()
if time >= self.next_slashfx:
wound = d * SQRT2 / self.distance
if self.next_slashfx <= 0:
play(self.sfx_slash, wound, enemy.get_angle())
self.next_slashfx = time + ATTACK_SPEED
self.next_slashfx = ATTACK_SPEED
enemy.hit(wound / self.hero.spin_speed)
if enemy.wound >= ENEMY_HP:
self.score += enemy.wound
@ -252,21 +250,21 @@ class Maze:
def track_bullets(self):
"""Handle the bullets."""
fallen, time = [], get_ticks()
fallen = []
if (self.hero.firing and not self.hero.slashing
and time >= self.hero.next_strike):
self.hero.next_strike = time + ATTACK_SPEED
and self.hero.next_strike <= 0):
self.hero.next_strike = ATTACK_SPEED
self.bullets.append(Bullet(self.surface, self.x, self.y,
self.hero.angle, 'Aluminium'))
for i, bullet in enumerate(self.bullets):
wound = float(bullet.fall_time-time) / BULLET_LIFETIME
wound = bullet.fall_time / BULLET_LIFETIME
bullet.update(self.fps, self.distance)
if wound < 0:
fallen.append(i)
elif bullet.color == 'Aluminium':
x = MIDDLE + round2((bullet.x-self.x) / self.distance)
y = MIDDLE + round2((bullet.y-self.y) / self.distance)
if self.map[x][y] == WALL and time >= self.next_move:
if self.map[x][y] == WALL and self.next_move <= 0:
fallen.append(i)
continue
for j, enemy in enumerate(self.enemies):
@ -282,9 +280,9 @@ class Maze:
fallen.append(i)
break
elif bullet.get_distance(self.x, self.y) < self.distance:
if self.hero.spin_queue and time >= self.hero.next_heal:
if self.hero.spin_queue and self.hero.next_heal <= 0:
self.hero.next_strike = (abs(self.hero.spin_queue*self.fps)
+ time + ATTACK_SPEED)
+ ATTACK_SPEED)
play(bullet.sfx_missed, wound, bullet.angle + pi)
else:
self.hit_hero(wound, bullet.color)
@ -319,6 +317,9 @@ class Maze:
dy = self.is_valid_move(vy=self.vy)
self.centery += dy
self.next_move -= 1000.0 / self.fps
self.next_slashfx -= 1000.0 / self.fps
if dx or dy:
self.rotate()
for enemy in self.enemies: enemy.wake()
@ -370,7 +371,7 @@ class Maze:
self.enemy_weights = {color: MINW for color in ENEMIES}
self.add_enemy()
self.next_move = self.next_slashfx = 0
self.next_move = self.next_slashfx = 0.0
self.hero.next_heal = self.hero.next_strike = 0
self.hero.slashing = self.hero.firing = self.hero.dead = False
self.hero.spin_queue = self.hero.wound = 0.0

View File

@ -21,8 +21,6 @@ __doc__ = 'Brutal Maze module for weapon classes'
from math import cos, sin
from pygame.time import get_ticks
from .constants import (BULLET_LIFETIME, SFX_SHOT_ENEMY, SFX_SHOT_HERO,
SFX_MISSED, BULLET_SPEED, ENEMY_HP, TANGO, BG_COLOR)
from .misc import regpoly, fill_aapolygon
@ -36,14 +34,14 @@ class Bullet:
x, y (int): coordinates of the center of the bullet (in pixels)
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
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
"""
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
self.fall_time = BULLET_LIFETIME
if color == 'Aluminium':
self.sfx_hit = SFX_SHOT_ENEMY
else:
@ -55,10 +53,11 @@ class Bullet:
s = distance * BULLET_SPEED / fps
self.x += s * cos(self.angle)
self.y += s * sin(self.angle)
self.fall_time -= 1000.0 / fps
def get_color(self):
"""Return current color of the enemy."""
value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
value = int((1 - self.fall_time/BULLET_LIFETIME) * ENEMY_HP)
try:
return TANGO[self.color][value]
except IndexError:

View File

@ -7,7 +7,7 @@ with open('README.rst') as f:
setup(
name='brutalmaze',
version='0.5.6',
version='0.6.0',
description='A minimalist hack and slash game with fast-paced action',
long_description=long_description,
url='https://github.com/McSinyx/brutalmaze',

2
wiki

@ -1 +1 @@
Subproject commit 7d455d5a9e1933963a86ea475008582ec961c22d
Subproject commit bcb956351f06ad378add2e65f43644fdae110ae9