Allow moving hero using mouse
This commit is contained in:
parent
92a41b3cff
commit
9dff378b57
11
README.rst
11
README.rst
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = [], []
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
6
setup.py
6
setup.py
|
@ -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
2
wiki
|
@ -1 +1 @@
|
|||
Subproject commit 8f40eb7b3d368076bb2b9fc4d268472af62e2886
|
||||
Subproject commit b4169d8f16a5f99b11f41e4823ca67065788cbac
|
Loading…
Reference in New Issue