Merge pull request #38 from n0remac/monster-kill-combos

Monster kill combos
This commit is contained in:
Lief9100
2020-04-22 23:25:47 -07:00
committed by GitHub
5 changed files with 236 additions and 37 deletions

View File

@@ -29,21 +29,25 @@ class Config(object):
# Constants used to scale our sprites from their original size
CHARACTER_SCALING = 1
TILE_SCALING = 2
TILE_SIZE = TILE_WIDTH * TILE_SCALING
# The number of pixels across the level
LEVEL_SIZE = 10 * TILE_SCALING * TILE_WIDTH
# Movement speed of player, in pixels per frame
PLAYER_MOVEMENT_SPEED = 7
PLAYER_MOVEMENT_SPEED = 14
# How many pixels to keep as a minimum margin between the characters and the edge of the screen.
LEFT_VIEWPORT_MARGIN = 250
RIGHT_VIEWPORT_MARGIN = 250
BOTTOM_VIEWPORT_MARGIN = 50
TOP_VIEWPORT_MARGIN = 100
LEFT_VIEWPORT_MARGIN = 700
RIGHT_VIEWPORT_MARGIN = 700
BOTTOM_VIEWPORT_MARGIN = 300
TOP_VIEWPORT_MARGIN = 350
# All debug statements and renderings should use this
DEBUG = True
DEBUG = False
# Monster Count to be spawned
MONSTER_COUNT = 10
class Enums(Enum):

View File

@@ -8,13 +8,14 @@ import collections
import math
import random
import time
from typing import Tuple, List
import arcade
from config import Config
from map import Dungeon
from mobs import Player
from mobs import Player, Enemy
from projectiles import Temp
from recipe import ActiveRecipe
class FPSCounter:
@@ -47,6 +48,7 @@ class Game(arcade.Window):
# Sprite Lists
self.enemy_list = None
self.active_enemies = []
self.bullet_list = None
self.player = None
# Game Objects
@@ -55,6 +57,7 @@ class Game(arcade.Window):
self.physics_engine = None # Our physics engine
# Used to keep track of our scrolling
self.view_bottom = self.view_left = 0
self.active_recipe = []
arcade.set_background_color(arcade.color.BLACK)
@@ -63,19 +66,41 @@ class Game(arcade.Window):
# Create the Sprite lists
self.enemy_list = arcade.SpriteList()
self.active_enemies = arcade.SpriteList()
self.fps = FPSCounter()
self.bullet_list = arcade.SpriteList()
# Create the dungeon
self.dungeon = Dungeon(0, 3)
# Set up recipes
self.active_recipe = ActiveRecipe()
self.active_recipe.set_ghosts()
# Set up the player, specifically placing it at these coordinates.
self.player = Player()
self.player = Player(self.dungeon)
self.player.scale = 1
level = random.choice(self.dungeon.levelList)
self.player.center_x, self.player.center_y = level.center()
self.player.cur_recipe = self.active_recipe.active
# x, y = level.center()
# Set up monsters
for count in range(Config.MONSTER_COUNT//2):
mob = Enemy(filename="resources/images/monsters/ghost/ghost1.png", dungeon=self.dungeon)
mob.center_x, mob.center_y = random.choice(self.dungeon.levelList).center()
mob.target = self.player
mob.scale = 4
mob.monster_type = 'ghost'
self.enemy_list.append(mob)
for count in range(Config.MONSTER_COUNT//2):
mob = Enemy(filename="resources/images/monsters/frog/frog1.png", dungeon=self.dungeon)
mob.center_x, mob.center_y = random.choice(self.dungeon.levelList).center()
mob.target = self.player
mob.scale = 4
mob.monster_type = 'frog'
self.enemy_list.append(mob)
# Setup viewport
self.view_bottom = self.player.center_x - (0.5 * Config.SCREEN_WIDTH) + 300
self.view_left = self.player.center_x - (0.5 * Config.SCREEN_WIDTH)
@@ -84,10 +109,6 @@ class Game(arcade.Window):
self.view_bottom,
Config.SCREEN_HEIGHT + self.view_bottom)
# Create monsters
# self.enemy_list.append(Enemy("resources/images/monsters/ghost/ghost1.png", 200, 200, 4))
# self.enemy_list.append(Enemy("resources/images/monsters/frog/frog1.png", 200, 1000, 4))
# Create the 'physics engine'
self.physics_engine = arcade.PhysicsEngineSimple(self.player, self.dungeon.getWalls())
@@ -101,22 +122,48 @@ class Game(arcade.Window):
self.dungeon.render()
self.player.draw()
self.enemy_list.draw()
self.active_enemies.draw()
self.bullet_list.draw()
self.active_recipe.render()
if Config.DEBUG:
x, y = self.player.position
tile = Config.TILE_WIDTH * Config.TILE_SCALING
arcade.draw_rectangle_outline(round(x / tile) * tile, round(y / tile) * tile, tile, tile,
arcade.color.RED)
arcade.draw_rectangle_outline(round(x / Config.TILE_SIZE) * Config.TILE_SIZE,
round(y / Config.TILE_SIZE) * Config.TILE_SIZE,
Config.TILE_SIZE, Config.TILE_SIZE, arcade.color.RED)
self.player.draw_hit_box()
arcade.draw_text(str((x, y)), x - 40, y + 50, arcade.color.WHITE, 15, font_name='Arial')
arcade.draw_text(f"FPS: {self.fps.get_fps():3.0f}", self.view_left + 50, self.view_bottom + 30,
arcade.color.WHITE, 16, font_name='Arial')
arcade.color.WHITE, 16, font_name='Arial')
# Draw paths for all mobs
for mob in self.active_enemies:
if mob.target is not None:
t1 = time.time()
path = mob.get_path()
t2 = time.time()
print(f'Path acquired in {round(t2 - t1, 4)}s')
self.draw_path(path)
mob.tick(path)
self.fps.tick()
except Exception:
import traceback
traceback.print_exc()
@staticmethod
def draw_path(path: List[Tuple[int, int]]) -> None:
"""
Draws a line between positions in a list of tuple, also known as the path.
:param path: A list of tuple positions defining a path that can be traversed.
"""
if len(path) > 2:
path = map(lambda point: ((point[0]) * Config.TILE_SIZE, (point[1]) * Config.TILE_SIZE), path)
path = list(path)
for pos1, pos2 in zip(path, path[1:]):
arcade.draw_line(*pos1, *pos2, color=arcade.color.RED)
def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed. """
@@ -134,6 +181,9 @@ class Game(arcade.Window):
self.prev_keypress.append(key)
elif key == 65307:
self.close()
elif key == 65505:
self.active_recipe.next_recipe()
self.player.cur_recipe = self.active_recipe.active
def on_key_release(self, key, modifiers):
"""Called when the user releases a key. """
@@ -150,6 +200,7 @@ class Game(arcade.Window):
elif key == arcade.key.RIGHT or key == arcade.key.D:
self.player.change_x = 0
self.prev_keypress.remove(key)
if self.prev_keypress:
self.on_key_press(self.prev_keypress.pop(0), 0)
@@ -157,6 +208,7 @@ class Game(arcade.Window):
"""
Called whenever the mouse is clicked.
"""
# Create a bullet TEMP SPRITE, currently wielding frog slingshot
bullet = Temp()
# Position the bullet at the player's current location
@@ -229,16 +281,39 @@ class Game(arcade.Window):
Config.SCREEN_WIDTH + self.view_left,
self.view_bottom,
Config.SCREEN_HEIGHT + self.view_bottom)
# Enemy activation and update
for enemy in reversed(self.enemy_list):
if (
enemy.bottom > self.view_bottom and
enemy.top < self.view_bottom + Config.SCREEN_HEIGHT and
enemy.right < self.view_left + Config.SCREEN_WIDTH and
enemy.left > self.view_left
):
if Config.DEBUG:
print("Activate Enemy")
self.active_enemies.append(enemy)
self.enemy_list.remove(enemy)
try:
for enemy in self.active_enemies:
enemy.update()
path = enemy.get_path()
enemy.tick(path)
except Exception:
import traceback
traceback.print_exc()
# Projectile updates
self.bullet_list.update()
for bullet in self.bullet_list:
# Collision Checks
hit_list = arcade.check_for_collision_with_list(bullet, self.dungeon.getWalls())
enemy_hit_list = arcade.check_for_collision_with_list(bullet, self.active_enemies)
# If it did, get rid of the bullet
if len(hit_list) > 0:
bullet.remove_from_sprite_lists()
if len(enemy_hit_list):
self.player.add_kill(enemy_hit_list[0].monster_type)
enemy_hit_list[0].remove_from_sprite_lists()
# If the bullet flies off-screen, remove it. TEMP change to range calc
if (

View File

@@ -7,11 +7,16 @@ Pathfinding will also depend on objects here, and is thus integral to it's funct
from __future__ import annotations
import json
from pprint import pprint
import arcade
import numpy as np
from itertools import chain
from config import Config
from pathfinding.core.diagonal_movement import DiagonalMovement
from pathfinding.core.grid import Grid
from pathfinding.finder.a_star import AStarFinder
class Dungeon(object):
@@ -40,6 +45,16 @@ class Dungeon(object):
self.levels = [
[Level.load_file(x, y, center) for y in range(size)] for x in range(size)
]
self.matrix = [[1 for yy in range(size * 10)] for xx in range(10 * size)]
for column in self.levels:
for level in column:
for xx in range(10):
for yy in range(10):
if level.structure[xx][yy] == 'w':
self.matrix[(level.x * 10) + xx][(level.y * 10) + yy] = 0
self.grid = Grid(matrix=self.matrix)
self.finder = AStarFinder(diagonal_movement=DiagonalMovement.always)
pprint(self.matrix, width=1000)
def getWalls(self) -> arcade.SpriteList:
"""

View File

@@ -3,9 +3,11 @@ mobs.py
Organizes all classes related to Mobs, Entities, Enemies, Players and Items.
"""
import arcade
from typing import List, Tuple
import arcade
from config import Config, Enums, SpritePaths
from map import Dungeon
from sprites import PlayerAnimations
@@ -14,9 +16,9 @@ class Mob(arcade.Sprite):
Represents a Mob. No defined behaviour, it has no intelligence.
"""
def __init__(self, max_health=100, max_armor=0, *args, **kwargs) -> None:
def __init__(self, dungeon: Dungeon, max_health=100, max_armor=0, *args, **kwargs) -> None:
# Set up parent class
super().__init__()
super(Mob, self).__init__(*args, **kwargs)
self.max_health, self.max_armor = max_health, max_armor
self.health, self.armor = max_health, max_armor
@@ -26,13 +28,10 @@ class Mob(arcade.Sprite):
self.down_textures = []
self.cur_texture = 0
def tick(self) -> None:
"""
A on_update function, the Mob should decide it's next actions here.
"""
pass
self.dungeon = dungeon
self.target = None
class Player(Mob):
"""
Represents a Player.
@@ -55,6 +54,19 @@ class Player(Mob):
self.refreshIndex = 0
self.prev = Enums.IDLE
self.texture = next(self.map[self.prev])
self.kill_list = []
self.cur_recipe = None
def add_kill(self, creature):
# Adds a kill to kill_list. If 3 or more check the recipe then give a power up if it matches.
self.kill_list.append(creature)
print(self.kill_list)
print(self.cur_recipe)
if self.cur_recipe == self.kill_list:
print("+++++++++++++++++++++++++++++++++++++++++++++++++++++++")
self.kill_list = []
elif len(self.kill_list) >= 3:
self.kill_list = []
def update_animation(self, delta_time: float = 1 / 60) -> None:
"""
@@ -101,17 +113,51 @@ class Enemy(Mob):
def __init__(self, *args, **kwargs) -> None:
super(Enemy, self).__init__(*args, **kwargs)
self.monster_type = ''
def tick(self) -> None:
def nearestPosition(self) -> Tuple[int, int]:
"""
A on_update function, the Enemy Mob should scan for the player, decide how to path to it, and
decide how to take offensive action.
"""
pass
Returns the nearest absolute dungeon tile the Mob is placed on.
def path(self) -> None:
:return: A tuple containing the Mob's dungeon tile position.
"""
Not yet decided how this function should work.
Basically, most pathfinding decisions should be kept within this function.
return (round(self.center_x / Config.TILE_SIZE),
round(self.center_y / Config.TILE_SIZE))
def tick(self, path: Tuple[int, int] = None) -> None:
"""
pass
A on_update function, the Mob should decide it's next actions here.
"""
curpos, nextpos = self.nearestPosition(), path[1]
# print(curpos, nextpos)
if nextpos[0] > curpos[0]:
self.change_x = Config.PLAYER_MOVEMENT_SPEED - 3
elif nextpos[0] < curpos[0]:
self.change_x = -Config.PLAYER_MOVEMENT_SPEED + 3
else:
self.change_x = 0
if nextpos[1] > curpos[1]:
self.change_y = Config.PLAYER_MOVEMENT_SPEED - 3
elif nextpos[1] < curpos[1]:
self.change_y = -Config.PLAYER_MOVEMENT_SPEED + 3
else:
self.change_y = 0
# print(self.change_x, self.change_y)
def get_path(self, end: Tuple[int, int] = None) -> List[Tuple[int, int]]:
"""
Returns the path to get to the Mob's target in absolute integer positions.
:param end: A the endpoint tuple. Must be a valid position within the matrix.
:return:
"""
if end is None:
end = self.target.position
start, end = self.nearestPosition(), (round(end[0] / Config.TILE_SIZE), round(end[1] / Config.TILE_SIZE))
start, end = self.dungeon.grid.node(*start), self.dungeon.grid.node(*end)
paths, runs = self.dungeon.finder.find_path(start, end, self.dungeon.grid)
self.dungeon.grid.cleanup()
return paths

59
triple-dungeon/recipe.py Normal file
View File

@@ -0,0 +1,59 @@
'''
Recipes are combinations of three monsters. When a player fills a recipe they get an updgrade
'''
import arcade
class Recipe:
'''
A class of different recipes
'''
GHOSTS = ['ghost', 'ghost', 'ghost']
FROGS = ['frog', 'frog', 'frog']
GHOST_FROG = ['ghost', 'ghost', 'frog']
FROG_GHOST = ['ghost', 'frog', 'frog']
class ActiveRecipe(arcade.SpriteList):
'''
Keeps track of the active recipe and draws it.
'''
def __init__(self):
super().__init__()
self.active = Recipe.GHOSTS
self.cycle_recipes = [self.set_frogs, self.set_ghosts]
self.pos = 0
def render(self) -> None:
x = 0
for sprite in self.sprite_list:
screen_right = arcade.get_viewport()[1] - 100
screen_top = arcade.get_viewport()[3] - 80
sprite.scale = 4
sprite.center_x = screen_right - x
sprite.center_y = screen_top
x += 70
sprite.draw()
def next_recipe(self):
self.cycle_recipes[self.pos]()
self.pos += 1
if self.pos == len(self.cycle_recipes):
self.pos = 0
def set_ghosts(self) -> None:
self.active = Recipe.GHOSTS
self.sprite_list = []
self.sprite_list.append(arcade.Sprite(filename="resources/images/monsters/ghost/ghost1.png"))
self.sprite_list.append(arcade.Sprite(filename="resources/images/monsters/ghost/ghost1.png"))
self.sprite_list.append(arcade.Sprite(filename="resources/images/monsters/ghost/ghost1.png"))
def set_frogs(self) -> None:
self.active = Recipe.FROGS
self.sprite_list = []
self.sprite_list.append(arcade.Sprite(filename="resources/images/monsters/frog/frog1.png"))
self.sprite_list.append(arcade.Sprite(filename="resources/images/monsters/frog/frog1.png"))
self.sprite_list.append(arcade.Sprite(filename="resources/images/monsters/frog/frog1.png"))