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 is a hack and slash game with fast-paced action and a minimalist
art style.
Brutal Maze is a third-person shooter game with fast-paced action and a
minimalist art style.
.. image:: https://raw.githubusercontent.com/McSinyx/brutalmaze/master/screenshot.png
@ -39,7 +39,8 @@ For more information, see
page from Brutal Maze wiki.
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
New game.
@ -55,9 +56,11 @@ Up
Move up.
Down
Move down.
Right Mouse
Move the hero using mouse
Left Mouse
Long-range attack.
Right Mouse
Space
Close-range attack, also dodge from bullets.
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 (
TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY,
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 .weapons import Bullet
@ -232,8 +232,8 @@ class Enemy:
self.move_speed = self.maze.fps / speed
directions = [(sign(MIDDLE - self.x), 0), (0, sign(MIDDLE - self.y))]
shuffle(directions)
directions.append(choice(ADJACENT_GRIDS))
if self.maze.hero.dead: directions = choice(ADJACENT_GRIDS),
directions.append(choice(ADJACENTS))
if self.maze.hero.dead: directions = choice(ADJACENTS),
for x, y in directions:
if (x or y) and self.maze.map[self.x + x][self.y + y] == EMPTY:
self.offsetx = round(x * (1 - self.move_speed))

View File

@ -56,9 +56,9 @@ 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)
ADJACENT_GRIDS = (1, 0), (0, 1), (-1, 0), (0, -1)
AROUND_HERO = set((MIDDLE + x, MIDDLE + y) for x, y in
ADJACENT_GRIDS + ((1, 1), (-1, 1), (-1, -1), (1, -1)))
ADJACENTS = (1, 0), (0, 1), (-1, 0), (0, -1)
CORNERS = (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)),
'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)),

View File

@ -1,5 +1,5 @@
# -*- 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
#
# This file is part of Brutal Maze.
@ -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.6.5'
__version__ = '0.7.0'
import re
from argparse import ArgumentParser, FileType, RawTextHelpFormatter
@ -37,7 +37,7 @@ from pygame import KEYDOWN, QUIT, VIDEORESIZE
from pygame.time import Clock, get_ticks
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 .misc import deg, round2, sign
@ -50,6 +50,7 @@ class ConfigReader:
('Toggle mute', 'mute'),
('Move left', 'left'), ('Move right', 'right'),
('Move up', 'up'), ('Move down', 'down'),
('Auto move', 'autove'),
('Long-range attack', 'shot'),
('Close-range attack', 'slash'))
WEIRD_MOUSE_ERR = '{}: Mouse is not a suitable control'
@ -81,7 +82,7 @@ class ConfigReader:
for cmd, alias in self.CONTROL_ALIASES:
i = self.config.get('Control', cmd)
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))
self.mouse[alias] = int(i[-1]) - 1
continue
@ -219,27 +220,33 @@ class Game:
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
velocity = self.maze.distance * HERO_SPEED / self.fps
maze = self.maze
velocity = maze.distance * HERO_SPEED / self.fps
accel = velocity * HERO_SPEED / self.fps
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:
self.maze.vx += x * 2 * accel
if x == y == 0:
maze.set_step()
x, y = maze.stepx, maze.stepy
else:
self.maze.vx += x * accel
if abs(self.maze.vx) > velocity: self.maze.vx = x * velocity
x, y = -x, -y # or move the maze in the reverse direction
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:
self.maze.vy += y * 2 * accel
if maze.next_move > 0 or not x:
maze.vx -= sign(maze.vx) * accel
if abs(maze.vx) < accel * 2: maze.vx = 0.0
elif x * maze.vx < 0:
maze.vx += x * 2 * accel
else:
self.maze.vy += y * accel
if abs(self.maze.vy) > velocity: self.maze.vy = y * velocity
maze.vx += x * accel
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):
"""Control how the hero move and attack."""
@ -293,11 +300,11 @@ class Game:
right = keys[self.key['right']] - keys[self.key['left']]
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()
try:
autove = keys[self.key['autove']]
except KeyError:
autove = buttons[self.mouse['autove']]
try:
firing = keys[self.key['shot']]
except KeyError:
@ -307,6 +314,21 @@ class Game:
except KeyError:
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)
def __exit__(self, exc_type, exc_value, traceback):

View File

@ -19,7 +19,7 @@
__doc__ = 'Brutal Maze module for the maze class'
from collections import deque
from collections import deque, defaultdict
from math import pi, log
from random import choice, getrandbits, uniform
@ -28,10 +28,10 @@ import pygame
from .characters import Hero, new_enemy
from .constants import (
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,
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
@ -74,6 +74,8 @@ class Maze:
enemy_weights (dict): probabilities of enemies to be created
enemies (list of Enemy): alive enemies
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_slashfx (float): time until next slash effect of the hero (in ms)
slashd (float): minimum distance for slashes to be effective
@ -105,6 +107,8 @@ class Maze:
self.add_enemy()
self.hero = Hero(self.surface, fps, size)
self.map[MIDDLE][MIDDLE] = HERO
self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0
self.next_move = self.next_slashfx = 0.0
self.slashd = self.hero.R + self.distance/SQRT2
@ -121,7 +125,7 @@ class Maze:
num = log(self.score, INIT_SCORE)
while walls and len(self.enemies) < num:
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
enemy = new_enemy(self, x, y)
self.enemies.append(enemy)
@ -164,22 +168,26 @@ class Maze:
y = int((self.centery-self.y) * 2 / self.distance)
if x == y == 0: return
for enemy in self.enemies: self.map[enemy.x][enemy.y] = EMPTY
self.map[MIDDLE][MIDDLE] = EMPTY
if x:
self.centerx -= x * self.distance
self.map.rotate(x)
self.rotatex += x
if y:
self.centery -= y * self.distance
for d in self.map: d.rotate(y)
self.rotatey += y
self.centerx -= x * self.distance
self.map.rotate(x)
self.rotatex += x
self.centery -= y * self.distance
for d in self.map: d.rotate(y)
self.rotatey += y
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
killist = []
for i, enemy in enumerate(self.enemies):
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
enemy.die()
killist.append(i)
@ -307,26 +315,25 @@ class Maze:
return 0.0
for enemy in self.enemies:
x, y = self.get_pos(enemy.x, enemy.y)
if (max(abs(herox - x), abs(heroy - y)) * 2 < self.distance
and enemy.awake):
if max(abs(herox - x), abs(heroy - y)) * 2 < self.distance:
return 0.0
return vx or vy
def update(self, fps):
"""Update the maze."""
self.fps = fps
dx = self.is_valid_move(vx=self.vx)
self.centerx += dx
dy = self.is_valid_move(vy=self.vy)
self.centery += dy
self.vx = self.is_valid_move(vx=self.vx)
self.centerx += self.vx
self.vy = self.is_valid_move(vy=self.vy)
self.centery += self.vy
self.next_move -= 1000.0 / self.fps
self.next_slashfx -= 1000.0 / self.fps
self.rotate()
if dx or dy:
if self.vx or self.vy:
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()
if not self.hero.dead:
@ -351,6 +358,44 @@ class Maze:
self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
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):
"""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
@ -359,6 +404,8 @@ class Maze:
"""Handle loses."""
self.hero.dead = True
self.hero.slashing = self.hero.firing = False
self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0
self.vx = self.vy = 0.0
play(self.sfx_lose)
@ -368,6 +415,8 @@ class Maze:
self.score = INIT_SCORE
self.map = deque()
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.rotatex = self.rotatey = 0
self.bullets, self.enemies = [], []

View File

@ -19,12 +19,15 @@
__doc__ = 'Brutal Maze module for miscellaneous functions'
from itertools import chain
from math import degrees, cos, sin, pi
from random import uniform
from random import shuffle, uniform
import pygame
from pygame.gfxdraw import filled_polygon, aapolygon
from .constants import ADJACENTS, CORNERS
def round2(number):
"""Round a number to an int."""
@ -69,6 +72,15 @@ def cosin(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):
"""Choose a random key from a dict which has values being relative
weights of the coresponding keys.

View File

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

View File

@ -7,8 +7,8 @@ with open('README.rst') as f:
setup(
name='brutalmaze',
version='0.6.5',
description='A minimalist hack and slash game with fast-paced action',
version='0.7.0',
description='A minimalist TPS game with fast-paced action',
long_description=long_description,
url='https://github.com/McSinyx/brutalmaze',
author='Nguyễn Gia Phong',
@ -25,7 +25,7 @@ setup(
'Operating System :: OS Independent',
'Programming Language :: Python',
'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'],
install_requires=['appdirs', 'pygame>=1.9'],
package_data={'brutalmaze': ['icon.png', 'soundfx/*.ogg', 'settings.ini']},

2
wiki

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