diff --git a/triple-dungeon/config.py b/triple-dungeon/config.py index a395e94..393d685 100644 --- a/triple-dungeon/config.py +++ b/triple-dungeon/config.py @@ -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): diff --git a/triple-dungeon/main.py b/triple-dungeon/main.py index 4d3b3d4..2ecb526 100644 --- a/triple-dungeon/main.py +++ b/triple-dungeon/main.py @@ -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 ( diff --git a/triple-dungeon/map.py b/triple-dungeon/map.py index 8394d7b..32f254e 100644 --- a/triple-dungeon/map.py +++ b/triple-dungeon/map.py @@ -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: """ diff --git a/triple-dungeon/mobs.py b/triple-dungeon/mobs.py index 9b9906b..9560f13 100644 --- a/triple-dungeon/mobs.py +++ b/triple-dungeon/mobs.py @@ -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 diff --git a/triple-dungeon/recipe.py b/triple-dungeon/recipe.py new file mode 100644 index 0000000..2a35ca0 --- /dev/null +++ b/triple-dungeon/recipe.py @@ -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"))