Allow moving hero using mouse

This commit is contained in:
Nguyễn Gia Phong 2018-05-20 20:33:50 +07:00
parent 92a41b3cff
commit 9dff378b57
10 changed files with 150 additions and 62 deletions

View File

@ -1,8 +1,8 @@
Brutal Maze Brutal Maze
=========== ===========
Brutal Maze is a hack and slash game with fast-paced action and a minimalist Brutal Maze is a third-person shooter game with fast-paced action and a
art style. minimalist art style.
.. image:: https://raw.githubusercontent.com/McSinyx/brutalmaze/master/screenshot.png .. image:: https://raw.githubusercontent.com/McSinyx/brutalmaze/master/screenshot.png
@ -39,7 +39,8 @@ For more information, see
page from Brutal Maze wiki. page from Brutal Maze wiki.
After installation, you can launch the game by running the command After installation, you can launch the game by running the command
``brutalmaze``. Below are default bindings: ``brutalmaze``. Below are the default bindings, which can be configured as
shown in the next section:
F2 F2
New game. New game.
@ -55,9 +56,11 @@ Up
Move up. Move up.
Down Down
Move down. Move down.
Right Mouse
Move the hero using mouse
Left Mouse Left Mouse
Long-range attack. Long-range attack.
Right Mouse Space
Close-range attack, also dodge from bullets. Close-range attack, also dodge from bullets.
Configuration Configuration

View File

@ -1 +1 @@
"""Brutal Maze is a minimalist hack and slash game with fast-paced action""" """Brutal Maze is a minimalist third-person shooter with fast-paced action"""

View File

@ -26,7 +26,7 @@ from sys import modules
from .constants import ( from .constants import (
TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY, TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY,
ENEMY_SPEED, ENEMY_HP, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, AROUND_HERO, ENEMY_SPEED, ENEMY_HP, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, AROUND_HERO,
ADJACENT_GRIDS, EMPTY, FG_COLOR, SQRT2, MINW) ADJACENTS, EMPTY, FG_COLOR, SQRT2, MINW)
from .misc import sign, cosin, randsign, regpoly, fill_aapolygon, choices, play from .misc import sign, cosin, randsign, regpoly, fill_aapolygon, choices, play
from .weapons import Bullet from .weapons import Bullet
@ -232,8 +232,8 @@ class Enemy:
self.move_speed = self.maze.fps / speed self.move_speed = self.maze.fps / speed
directions = [(sign(MIDDLE - self.x), 0), (0, sign(MIDDLE - self.y))] directions = [(sign(MIDDLE - self.x), 0), (0, sign(MIDDLE - self.y))]
shuffle(directions) shuffle(directions)
directions.append(choice(ADJACENT_GRIDS)) directions.append(choice(ADJACENTS))
if self.maze.hero.dead: directions = choice(ADJACENT_GRIDS), if self.maze.hero.dead: directions = choice(ADJACENTS),
for x, y in directions: for x, y in directions:
if (x or y) and self.maze.map[self.x + x][self.y + y] == EMPTY: if (x or y) and self.maze.map[self.x + x][self.y + y] == EMPTY:
self.offsetx = round(x * (1 - self.move_speed)) self.offsetx = round(x * (1 - self.move_speed))

View File

@ -56,9 +56,9 @@ ATTACK_SPEED = 333.333 # ms/strike
FIRANGE = 6 # grids FIRANGE = 6 # grids
BULLET_LIFETIME = 1000.0 * FIRANGE / (BULLET_SPEED-HERO_SPEED) # ms BULLET_LIFETIME = 1000.0 * FIRANGE / (BULLET_SPEED-HERO_SPEED) # ms
EMPTY, WALL, HERO, ENEMY = range(4) EMPTY, WALL, HERO, ENEMY = range(4)
ADJACENT_GRIDS = (1, 0), (0, 1), (-1, 0), (0, -1) ADJACENTS = (1, 0), (0, 1), (-1, 0), (0, -1)
AROUND_HERO = set((MIDDLE + x, MIDDLE + y) for x, y in CORNERS = (1, 1), (-1, 1), (-1, -1), (1, -1)
ADJACENT_GRIDS + ((1, 1), (-1, 1), (-1, -1), (1, -1))) AROUND_HERO = set((MIDDLE + x, MIDDLE + y) for x, y in ADJACENTS + CORNERS)
TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)), TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)), 'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)),

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# main.py - main module, starts game and main loop # game.py - main module, starts game and main loop
# Copyright (C) 2017, 2018 Nguyễn Gia Phong # Copyright (C) 2017, 2018 Nguyễn Gia Phong
# #
# This file is part of Brutal Maze. # This file is part of Brutal Maze.
@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__version__ = '0.6.5' __version__ = '0.7.0'
import re import re
from argparse import ArgumentParser, FileType, RawTextHelpFormatter from argparse import ArgumentParser, FileType, RawTextHelpFormatter
@ -37,7 +37,7 @@ from pygame import KEYDOWN, QUIT, VIDEORESIZE
from pygame.time import Clock, get_ticks from pygame.time import Clock, get_ticks
from appdirs import AppDirs from appdirs import AppDirs
from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED, COLORS, WALL from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED, COLORS, MIDDLE, WALL
from .maze import Maze from .maze import Maze
from .misc import deg, round2, sign from .misc import deg, round2, sign
@ -50,6 +50,7 @@ class ConfigReader:
('Toggle mute', 'mute'), ('Toggle mute', 'mute'),
('Move left', 'left'), ('Move right', 'right'), ('Move left', 'left'), ('Move right', 'right'),
('Move up', 'up'), ('Move down', 'down'), ('Move up', 'up'), ('Move down', 'down'),
('Auto move', 'autove'),
('Long-range attack', 'shot'), ('Long-range attack', 'shot'),
('Close-range attack', 'slash')) ('Close-range attack', 'slash'))
WEIRD_MOUSE_ERR = '{}: Mouse is not a suitable control' WEIRD_MOUSE_ERR = '{}: Mouse is not a suitable control'
@ -81,7 +82,7 @@ class ConfigReader:
for cmd, alias in self.CONTROL_ALIASES: for cmd, alias in self.CONTROL_ALIASES:
i = self.config.get('Control', cmd) i = self.config.get('Control', cmd)
if re.match('mouse[1-3]$', i.lower()): if re.match('mouse[1-3]$', i.lower()):
if alias not in ('shot', 'slash'): if alias not in ('autove', 'shot', 'slash'):
raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd)) raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
self.mouse[alias] = int(i[-1]) - 1 self.mouse[alias] = int(i[-1]) - 1
continue continue
@ -219,27 +220,33 @@ class Game:
def move(self, x, y): def move(self, x, y):
"""Command the hero to move faster in the given direction.""" """Command the hero to move faster in the given direction."""
x, y = -x, -y # or move the maze in the reverse direction maze = self.maze
velocity = self.maze.distance * HERO_SPEED / self.fps velocity = maze.distance * HERO_SPEED / self.fps
accel = velocity * HERO_SPEED / self.fps accel = velocity * HERO_SPEED / self.fps
if self.maze.next_move > 0 or not x: if x == y == 0:
self.maze.vx -= sign(self.maze.vx) * accel maze.set_step()
if abs(self.maze.vx) < accel * 2: self.maze.vx = 0.0 x, y = maze.stepx, maze.stepy
elif x * self.maze.vx < 0:
self.maze.vx += x * 2 * accel
else: else:
self.maze.vx += x * accel x, y = -x, -y # or move the maze in the reverse direction
if abs(self.maze.vx) > velocity: self.maze.vx = x * velocity
if self.maze.next_move > 0 or not y: if maze.next_move > 0 or not x:
self.maze.vy -= sign(self.maze.vy) * accel maze.vx -= sign(maze.vx) * accel
if abs(self.maze.vy) < accel * 2: self.maze.vy = 0.0 if abs(maze.vx) < accel * 2: maze.vx = 0.0
elif y * self.maze.vy < 0: elif x * maze.vx < 0:
self.maze.vy += y * 2 * accel maze.vx += x * 2 * accel
else: else:
self.maze.vy += y * accel maze.vx += x * accel
if abs(self.maze.vy) > velocity: self.maze.vy = y * velocity if abs(maze.vx) > velocity: maze.vx = x * velocity
if maze.next_move > 0 or not y:
maze.vy -= sign(maze.vy) * accel
if abs(maze.vy) < accel * 2: maze.vy = 0.0
elif y * maze.vy < 0:
maze.vy += y * 2 * accel
else:
maze.vy += y * accel
if abs(maze.vy) > velocity: maze.vy = y * velocity
def control(self, x, y, angle, firing, slashing): def control(self, x, y, angle, firing, slashing):
"""Control how the hero move and attack.""" """Control how the hero move and attack."""
@ -293,11 +300,11 @@ class Game:
right = keys[self.key['right']] - keys[self.key['left']] right = keys[self.key['right']] - keys[self.key['left']]
down = keys[self.key['down']] - keys[self.key['up']] down = keys[self.key['down']] - keys[self.key['up']]
# Follow the mouse cursor
x, y = pygame.mouse.get_pos()
angle = atan2(y - self.hero.y, x - self.hero.x)
buttons = pygame.mouse.get_pressed() buttons = pygame.mouse.get_pressed()
try:
autove = keys[self.key['autove']]
except KeyError:
autove = buttons[self.mouse['autove']]
try: try:
firing = keys[self.key['shot']] firing = keys[self.key['shot']]
except KeyError: except KeyError:
@ -307,6 +314,21 @@ class Game:
except KeyError: except KeyError:
slashing = buttons[self.mouse['slash']] slashing = buttons[self.mouse['slash']]
# Follow the mouse cursor
x, y = pygame.mouse.get_pos()
maze = self.maze
if right or down:
maze.destx = maze.desty = MIDDLE
maze.stepx = maze.stepy = 0
elif autove:
maze.destx = MIDDLE + round2((x-maze.centerx) / maze.distance)
maze.desty = MIDDLE + round2((y-maze.centery) / maze.distance)
maze.set_step(lambda x: maze.rangex[0] <= x <= maze.rangex[-1],
lambda y: maze.rangey[0] <= y <= maze.rangey[-1])
if maze.stepx == maze.stepy == 0:
maze.destx = maze.desty = MIDDLE
angle = atan2(y - self.hero.y, x - self.hero.x)
self.control(right, down, angle, firing, slashing) self.control(right, down, angle, firing, slashing)
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):

View File

@ -19,7 +19,7 @@
__doc__ = 'Brutal Maze module for the maze class' __doc__ = 'Brutal Maze module for the maze class'
from collections import deque from collections import deque, defaultdict
from math import pi, log from math import pi, log
from random import choice, getrandbits, uniform from random import choice, getrandbits, uniform
@ -28,10 +28,10 @@ import pygame
from .characters import Hero, new_enemy from .characters import Hero, new_enemy
from .constants import ( from .constants import (
EMPTY, WALL, HERO, ROAD_WIDTH, MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, EMPTY, WALL, HERO, ROAD_WIDTH, MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES,
MINW, MAXW, SQRT2, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_LOSE, ADJACENT_GRIDS, MINW, MAXW, SQRT2, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_LOSE, ADJACENTS,
BG_COLOR, FG_COLOR, CELL_WIDTH, LAST_ROW, HERO_HP, ENEMY_HP, ATTACK_SPEED, BG_COLOR, FG_COLOR, CELL_WIDTH, LAST_ROW, HERO_HP, ENEMY_HP, ATTACK_SPEED,
HERO_SPEED, BULLET_LIFETIME) HERO_SPEED, BULLET_LIFETIME)
from .misc import round2, sign, regpoly, fill_aapolygon, play from .misc import round2, sign, around, regpoly, fill_aapolygon, play
from .weapons import Bullet from .weapons import Bullet
@ -74,6 +74,8 @@ class Maze:
enemy_weights (dict): probabilities of enemies to be created enemy_weights (dict): probabilities of enemies to be created
enemies (list of Enemy): alive enemies enemies (list of Enemy): alive enemies
hero (Hero): the hero hero (Hero): the hero
destx, desty (int): the grid the hero is moving to
stepx, stepy (int): direction the maze is moving
next_move (float): time until the hero gets mobilized (in ms) next_move (float): time until the hero gets mobilized (in ms)
next_slashfx (float): time until next slash effect of the hero (in ms) next_slashfx (float): time until next slash effect of the hero (in ms)
slashd (float): minimum distance for slashes to be effective slashd (float): minimum distance for slashes to be effective
@ -105,6 +107,8 @@ class Maze:
self.add_enemy() self.add_enemy()
self.hero = Hero(self.surface, fps, size) self.hero = Hero(self.surface, fps, size)
self.map[MIDDLE][MIDDLE] = HERO self.map[MIDDLE][MIDDLE] = HERO
self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0
self.next_move = self.next_slashfx = 0.0 self.next_move = self.next_slashfx = 0.0
self.slashd = self.hero.R + self.distance/SQRT2 self.slashd = self.hero.R + self.distance/SQRT2
@ -121,7 +125,7 @@ class Maze:
num = log(self.score, INIT_SCORE) num = log(self.score, INIT_SCORE)
while walls and len(self.enemies) < num: while walls and len(self.enemies) < num:
x, y = choice(walls) x, y = choice(walls)
if all(self.map[x + a][y + b] == WALL for a, b in ADJACENT_GRIDS): if all(self.map[x + a][y + b] == WALL for a, b in ADJACENTS):
continue continue
enemy = new_enemy(self, x, y) enemy = new_enemy(self, x, y)
self.enemies.append(enemy) self.enemies.append(enemy)
@ -164,22 +168,26 @@ class Maze:
y = int((self.centery-self.y) * 2 / self.distance) y = int((self.centery-self.y) * 2 / self.distance)
if x == y == 0: return if x == y == 0: return
for enemy in self.enemies: self.map[enemy.x][enemy.y] = EMPTY for enemy in self.enemies: self.map[enemy.x][enemy.y] = EMPTY
self.map[MIDDLE][MIDDLE] = EMPTY self.map[MIDDLE][MIDDLE] = EMPTY
if x: self.centerx -= x * self.distance
self.centerx -= x * self.distance self.map.rotate(x)
self.map.rotate(x) self.rotatex += x
self.rotatex += x self.centery -= y * self.distance
if y: for d in self.map: d.rotate(y)
self.centery -= y * self.distance self.rotatey += y
for d in self.map: d.rotate(y)
self.rotatey += y
self.map[MIDDLE][MIDDLE] = HERO self.map[MIDDLE][MIDDLE] = HERO
if self.map[self.destx][self.desty] != HERO:
self.destx += x
self.desty += y
self.stepx = self.stepy = 0
# Respawn the enemies that fall off the display # Respawn the enemies that fall off the display
killist = [] killist = []
for i, enemy in enumerate(self.enemies): for i, enemy in enumerate(self.enemies):
enemy.place(x, y) enemy.place(x, y)
if enemy.x not in self.rangex or enemy.y not in self.rangey: if not (self.rangex[0] <= enemy.x <= self.rangex[-1]
and self.rangey[0] <= enemy.y <= self.rangey[-1]):
self.score += enemy.wound self.score += enemy.wound
enemy.die() enemy.die()
killist.append(i) killist.append(i)
@ -307,26 +315,25 @@ class Maze:
return 0.0 return 0.0
for enemy in self.enemies: for enemy in self.enemies:
x, y = self.get_pos(enemy.x, enemy.y) x, y = self.get_pos(enemy.x, enemy.y)
if (max(abs(herox - x), abs(heroy - y)) * 2 < self.distance if max(abs(herox - x), abs(heroy - y)) * 2 < self.distance:
and enemy.awake):
return 0.0 return 0.0
return vx or vy return vx or vy
def update(self, fps): def update(self, fps):
"""Update the maze.""" """Update the maze."""
self.fps = fps self.fps = fps
dx = self.is_valid_move(vx=self.vx) self.vx = self.is_valid_move(vx=self.vx)
self.centerx += dx self.centerx += self.vx
dy = self.is_valid_move(vy=self.vy) self.vy = self.is_valid_move(vy=self.vy)
self.centery += dy self.centery += self.vy
self.next_move -= 1000.0 / self.fps self.next_move -= 1000.0 / self.fps
self.next_slashfx -= 1000.0 / self.fps self.next_slashfx -= 1000.0 / self.fps
self.rotate() self.rotate()
if dx or dy: if self.vx or self.vy:
for enemy in self.enemies: enemy.wake() for enemy in self.enemies: enemy.wake()
for bullet in self.bullets: bullet.place(dx, dy) for bullet in self.bullets: bullet.place(self.vx, self.vy)
for enemy in self.enemies: enemy.update() for enemy in self.enemies: enemy.update()
if not self.hero.dead: if not self.hero.dead:
@ -351,6 +358,44 @@ class Maze:
self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1)) self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
self.slashd = self.hero.R + self.distance/SQRT2 self.slashd = self.hero.R + self.distance/SQRT2
def set_step(self, xcheck=(lambda _: True), ycheck=(lambda _: True)):
"""Return direction on the shortest path to the destination."""
if self.stepx or self.stepy and self.vx == self.vy == 0.0:
x, y = MIDDLE - self.stepx, MIDDLE - self.stepy
if self.stepx and not self.stepy:
nextx = x - self.stepx
n = self.map[x][y - 1] == EMPTY == self.map[nextx][y - 1]
s = self.map[x][y + 1] == EMPTY == self.map[nextx][y + 1]
self.stepy = n - s
elif not self.stepx and self.stepy:
nexty = y - self.stepy
w = self.map[x - 1][y] == EMPTY == self.map[x - 1][nexty]
e = self.map[x + 1][y] == EMPTY == self.map[x + 1][nexty]
self.stepx = w - e
return
queue = defaultdict(list, {0: [(self.destx, self.desty)]})
visited, count, distance = set(), 1, 0
while count:
# Hashes of small intergers are themselves so queue is sorted
if not queue[distance]: distance += 1
x, y = queue[distance].pop()
count -= 1
if (x, y) not in visited:
visited.add((x, y))
else:
continue
dx, dy = MIDDLE - x, MIDDLE - y
if dx**2 + dy**2 <= 2:
self.stepx, self.stepy = dx, dy
return
for i, j in around(x, y):
if self.map[i][j] == EMPTY and xcheck(i) and ycheck(j):
queue[distance + 1].append((i, j))
count += 1
self.stepx, self.stepy = 0, 0
def isfast(self): def isfast(self):
"""Return if the hero is moving faster than HERO_SPEED.""" """Return if the hero is moving faster than HERO_SPEED."""
return (self.vx**2+self.vy**2)**0.5*self.fps > HERO_SPEED*self.distance return (self.vx**2+self.vy**2)**0.5*self.fps > HERO_SPEED*self.distance
@ -359,6 +404,8 @@ class Maze:
"""Handle loses.""" """Handle loses."""
self.hero.dead = True self.hero.dead = True
self.hero.slashing = self.hero.firing = False self.hero.slashing = self.hero.firing = False
self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0
self.vx = self.vy = 0.0 self.vx = self.vy = 0.0
play(self.sfx_lose) play(self.sfx_lose)
@ -368,6 +415,8 @@ class Maze:
self.score = INIT_SCORE self.score = INIT_SCORE
self.map = deque() self.map = deque()
for _ in range(MAZE_SIZE): self.map.extend(new_column()) for _ in range(MAZE_SIZE): self.map.extend(new_column())
self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0
self.vx = self.vy = 0.0 self.vx = self.vy = 0.0
self.rotatex = self.rotatey = 0 self.rotatex = self.rotatey = 0
self.bullets, self.enemies = [], [] self.bullets, self.enemies = [], []

View File

@ -19,12 +19,15 @@
__doc__ = 'Brutal Maze module for miscellaneous functions' __doc__ = 'Brutal Maze module for miscellaneous functions'
from itertools import chain
from math import degrees, cos, sin, pi from math import degrees, cos, sin, pi
from random import uniform from random import shuffle, uniform
import pygame import pygame
from pygame.gfxdraw import filled_polygon, aapolygon from pygame.gfxdraw import filled_polygon, aapolygon
from .constants import ADJACENTS, CORNERS
def round2(number): def round2(number):
"""Round a number to an int.""" """Round a number to an int."""
@ -69,6 +72,15 @@ def cosin(x):
return cos(x) + sin(x) return cos(x) + sin(x)
def around(x, y):
"""Return grids around the given one in random order."""
a = [(x + i, y + j) for i, j in ADJACENTS]
shuffle(a)
c = [(x + i, y + j) for i, j in CORNERS]
shuffle(c)
return chain(a, c)
def choices(d): def choices(d):
"""Choose a random key from a dict which has values being relative """Choose a random key from a dict which has values being relative
weights of the coresponding keys. weights of the coresponding keys.

View File

@ -22,8 +22,10 @@ Move left: Left
Move right: Right Move right: Right
Move up: Up Move up: Up
Move down: Down Move down: Down
# Move hero using mouse
Auto move: Mouse3
Long-range attack: Mouse1 Long-range attack: Mouse1
Close-range attack: Mouse3 Close-range attack: Space
[Server] [Server]
# Enabling remote control will disable control via keyboard and mouse. # Enabling remote control will disable control via keyboard and mouse.

View File

@ -7,8 +7,8 @@ with open('README.rst') as f:
setup( setup(
name='brutalmaze', name='brutalmaze',
version='0.6.5', version='0.7.0',
description='A minimalist hack and slash game with fast-paced action', description='A minimalist TPS game with fast-paced action',
long_description=long_description, long_description=long_description,
url='https://github.com/McSinyx/brutalmaze', url='https://github.com/McSinyx/brutalmaze',
author='Nguyễn Gia Phong', author='Nguyễn Gia Phong',
@ -25,7 +25,7 @@ setup(
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Topic :: Games/Entertainment :: Arcade'], 'Topic :: Games/Entertainment :: Arcade'],
keywords='pygame action-game arcade-game maze socket-server ai-challenges', keywords='pygame third-person-shooter arcade-game maze ai-challenges',
packages=['brutalmaze'], packages=['brutalmaze'],
install_requires=['appdirs', 'pygame>=1.9'], install_requires=['appdirs', 'pygame>=1.9'],
package_data={'brutalmaze': ['icon.png', 'soundfx/*.ogg', 'settings.ini']}, package_data={'brutalmaze': ['icon.png', 'soundfx/*.ogg', 'settings.ini']},

2
wiki

@ -1 +1 @@
Subproject commit 8f40eb7b3d368076bb2b9fc4d268472af62e2886 Subproject commit b4169d8f16a5f99b11f41e4823ca67065788cbac