From 925082f687534f117fe62ab6b4d3e76890a0fb31 Mon Sep 17 00:00:00 2001 From: archieruin Date: Sun, 31 May 2020 16:52:30 +0300 Subject: [PATCH] Game Over and Bullets Done! --- game/entities/bullet.py | 27 ++++- game/entities/player.py | 18 ++- game/entities/slime.py | 35 ++++-- game/game.py | 3 +- game/scenes/game_over.py | 109 +++++++++++++++++- game/scenes/play.py | 102 ++++++++++------ ...game_manager.py => game_states_manager.py} | 7 +- res/fonts/dpcomic.ttf | Bin 0 -> 8940 bytes 8 files changed, 244 insertions(+), 57 deletions(-) rename game/states/{game_manager.py => game_states_manager.py} (88%) create mode 100644 res/fonts/dpcomic.ttf diff --git a/game/entities/bullet.py b/game/entities/bullet.py index 416d454..872bd81 100644 --- a/game/entities/bullet.py +++ b/game/entities/bullet.py @@ -7,21 +7,21 @@ class Bullet(Entity, Sprite): - def __init__(self, x, y, radius, color, direction, speed=10): + def __init__(self, x, y, radius, direction, speed=10, damage=1): super().__init__(x, y, width=radius, height=radius) Sprite.__init__(self) self.__radius = radius - self.__color = color self.__direction = self.normalize_vector2(direction) - print(self.__direction) self.__speed = speed + self.__damage = damage self.__vel_x = speed * -self.__direction[0] self.__vel_y = speed * -self.__direction[1] self.image = self.load(str(settings.bullets_res / 'bullet.png')) self.rect = self.image.get_rect(topleft=self.get_pos()) def draw(self, screen): - pygame.draw.circle(screen, self.__color, (self._x, self._y), self.__radius) + screen.blit(self.image, (self._x, self._y)) + # pygame.draw.circle(screen, self.__color, (self._x, self._y), self.__radius) def update(self, dt): if self._x < settings.SCREEN_WIDTH and self._x > 0 and self._y < settings.SCREEN_HEIGHT and self._y > 0: @@ -30,3 +30,22 @@ def update(self, dt): self.rect = self.image.get_rect(topleft=self.get_pos()) else: self.kill() + + def get_damage(self): + return self.__damage + + def set_damage(self, damage): + self.__damage = damage + + def get_rect(self): + return self.rect + + def collidepoint(self, point): + if self.rect.collidepoint(point): + return True + return False + + def colliderect(self, rect): + if self.rect.colliderect(rect): + return True + return False diff --git a/game/entities/player.py b/game/entities/player.py index 5295cc3..04818d3 100644 --- a/game/entities/player.py +++ b/game/entities/player.py @@ -9,7 +9,7 @@ class Player(Entity, Sprite): - def __init__(self, x, y, width, height, speed=5, health=50): + def __init__(self, x, y, width, height, speed=5, health=10): super().__init__(x, y, width, height) Sprite.__init__(self) @@ -105,8 +105,8 @@ def take_damage(self, enemy_pos, damage): dir_y = pos[1] - enemy_pos[1] dir_x, dir_y = self.normalize_vector2((dir_x, dir_y)) if self._vx < self.__max_vel and self._vy < self.__max_vel: - self._vx += dir_x * 5 - self._vy += dir_y * 5 + self._vx += dir_x * 2 + self._vy += dir_y * 2 def get_rect(self): return self.rect @@ -164,3 +164,15 @@ def handle_events(self, events, crosshair): self.idle = True self.move_right = False self.move_left = False + + def get_health(self): + return self.__health + + def set_health(self, health): + self.__health = health + + def sub_health(self, health): + self.__health -= health + + def add_health(self, health): + self.__health += health diff --git a/game/entities/slime.py b/game/entities/slime.py index 03f7fa6..29b7ea6 100644 --- a/game/entities/slime.py +++ b/game/entities/slime.py @@ -16,8 +16,10 @@ def __init__(self, x, y, width, height, speed=3, health=5): self.__slime_res = settings.slime_res self.__speed = speed + self.__max_vel = speed * 5 self.__health = health - self.__throw_countdown = 0 + self.__hit_countdown = 0 + self.__last_damage_ticks = 0 self.__move_anim = [ self.scale(self.load(str(self.__slime_res / 'slime_run_anim_f0.png')), width, height), @@ -47,6 +49,9 @@ def update(self, dt): self.image = self.__anim[int(self.__frame)] self.rect = self.image.get_rect(topleft=self.get_pos()) + if self.__health <= 0: + self.kill() + def move_to_player(self, player_pos): self_pos = self.get_pos() dir_x = self_pos[0] - player_pos[0] @@ -57,19 +62,31 @@ def move_to_player(self, player_pos): self._vx = -dir_x * self.__speed self._vy = -dir_y * self.__speed - def throw(self, player_pos): + def throw(self, from_pos, force): self_pos = self.get_pos() - dir_x = self_pos[0] - player_pos[0] - dir_y = self_pos[1] - player_pos[1] + dir_x = self_pos[0] - from_pos[0] + dir_y = self_pos[1] - from_pos[1] dir_len = sqrt(pow(dir_x, 2) + pow(dir_y, 2)) if dir_x or dir_y: dir_x = dir_x / dir_len dir_y = dir_y / dir_len - self._vx += dir_x * 3 - self._vy += dir_y * 3 - - def take_damage(self): - pass # TODO + self._vx += dir_x * force + self._vy += dir_y * force + + def take_damage(self, from_pos, damage): + seconds_passed = (pygame.time.get_ticks() - self.__last_damage_ticks) / 1000 + if seconds_passed > 0.1: + self.__health -= damage + self.__last_damage_ticks = pygame.time.get_ticks() + self.__hit_countdown = 5 + pos = self.get_pos() + dir_x = pos[0] - from_pos[0] + dir_y = pos[1] - from_pos[1] + dir_x, dir_y = self.normalize_vector2((dir_x, dir_y)) + # if self._vx < self.__max_vel and self._vy < self.__max_vel: + # self._vx = dir_x * self.__speed + # self._vy = dir_y * self.__speed + self.throw(from_pos, 40) def get_health(self): return self.__health diff --git a/game/game.py b/game/game.py index e4ed119..7d8f1b5 100644 --- a/game/game.py +++ b/game/game.py @@ -1,7 +1,7 @@ import pygame from game import settings -from game.states.game_manager import GameStatesManager +from game.states.game_states_manager import GameStatesManager from game.states.game_states import GameStates @@ -21,6 +21,7 @@ def __init__(self): # Inti game states manager self.__game_states_manager = GameStatesManager(GameStates.MAIN_MENU) + self.__game_states_manager.set_game_over_scene(5) self.__pause = False # Run game loop diff --git a/game/scenes/game_over.py b/game/scenes/game_over.py index 887fe6b..d38dc30 100644 --- a/game/scenes/game_over.py +++ b/game/scenes/game_over.py @@ -1,16 +1,117 @@ +import pygame + +from game import settings +from game.entities.crosshair import Crosshair from game.scenes.scene import Scene +from game.states.game_states import GameStates class GameOverScene(Scene): - def __init__(self, gsm): + def __init__(self, gsm, waves): super().__init__(gsm) + # Crosshair + self.__crosshair = Crosshair(settings.SCREEN_WIDTH / 2, + settings.SCREEN_HEIGHT / 2, + 35, 35, + settings.ui_res, + 'crosshair.png') + + # Fonts + font_path = str(settings.fonts_res / 'dpcomic.ttf') + self.__small_font = pygame.font.Font(font_path, 45) + self.__large_font = pygame.font.Font(font_path, 100) + + # Title + self.__title = self.__large_font.render("Game Over", True, (180, 200, 210)) + + # Start text + self.__button_text = self.__small_font.render("Restart", True, (255, 255, 255)) + + # Waves text + waves_text = f"Yor record: {waves} waves." + self.__waves_text = self.__small_font.render(waves_text, True, (235, 235, 235)) + + # Play Button + button_press_path = str(settings.ui_res / "menu_button_press.png") + button_rel_path = str(settings.ui_res / "menu_button.png") + self.__button_props = (192, 64) + self.__button_press_img = pygame.transform.scale(pygame.image.load(button_press_path), self.__button_props) + self.__button_rel_img = pygame.transform.scale(pygame.image.load(button_rel_path), self.__button_props) + self.__button_image = self.__button_rel_img + self.__button_pressed = False + + # Positions + self.__title_pos = (settings.SCREEN_WIDTH / 2 - self.__title.get_width() / 2, + settings.SCREEN_HEIGHT / 2 - self.__title.get_height() / 2 - 80) + + self.__button_pos = (settings.SCREEN_WIDTH / 2 - self.__button_props[0] / 2, + settings.SCREEN_HEIGHT / 2 - self.__button_props[1] / 2) + + self.__button_text_pos = (self.__button_pos[0] + self.__button_text.get_width() / 2 - 25, + self.__button_pos[1] + self.__button_text.get_height() / 2 - 15) + + self.__waves_text_pos = (settings.SCREEN_WIDTH / 2 - self.__waves_text.get_width() / 2, + settings.SCREEN_HEIGHT - 50) + + # Rects + self.__button_rect = self.__button_image.get_rect(topleft=self.__button_pos) + self.__button_text_rect = self.__button_text.get_rect(center=self.__button_text_pos) + + # Effects + self.__ignore_mouse_clicks = False + self.__transition = False + self.__close_effect = pygame.Surface((settings.SCREEN_WIDTH, settings.SCREEN_HEIGHT)) + self.__close_effect_alpha = 0 + self.__close_effect.set_alpha(self.__close_effect_alpha) + self.__close_effect.fill((10, 10, 10)) + def draw(self, screen): - pass + screen.blit(self.__title, self.__title_pos) + screen.blit(self.__button_image, self.__button_pos) + screen.blit(self.__button_text, self.__button_text_pos) + screen.blit(self.__waves_text, self.__waves_text_pos) + self.__crosshair.draw(screen) + if self.__transition: + screen.blit(self.__close_effect, (0, 0)) def update(self, dt): - pass + self.__crosshair.update(dt) + if self.__transition: + self.__close_effect_alpha += 10 + self.__close_effect.set_alpha(self.__close_effect_alpha) + if self.__close_effect_alpha > 255 + 300: + self._gsm.set_state(GameStates.PLAY) def handle_events(self, events): - pass + keys = pygame.key.get_pressed() + + if keys[pygame.K_SPACE] == 1: + if not self.__button_pressed: + self.__ignore_mouse_clicks = True + self.__button_image = self.__button_press_img + self.__button_pressed = True + self.__button_text_pos = (self.__button_pos[0] + self.__button_text.get_width() / 2 - 25, + self.__button_pos[1] + self.__button_text.get_height() / 2 - 8) + self.__transition = True + + if not self.__ignore_mouse_clicks: + press = pygame.mouse.get_pressed() + mouse_pos = pygame.mouse.get_pos() + collude_button = self.__button_rect.collidepoint(mouse_pos) or \ + self.__button_text_rect.collidepoint(mouse_pos) + if press[0]: + if collude_button and not self.__button_pressed: + self.__button_image = self.__button_press_img + self.__button_pressed = True + self.__button_text_pos = (self.__button_pos[0] + self.__button_text.get_width() / 2 - 25, + self.__button_pos[1] + self.__button_text.get_height() / 2 - 8) + else: + if self.__button_pressed: + self.__button_image = self.__button_rel_img + self.__button_pressed = False + self.__button_text_pos = (self.__button_pos[0] + self.__button_text.get_width() / 2 - 25, + self.__button_pos[1] + self.__button_text.get_height() / 2 - 15) + if collude_button: + self.__transition = True diff --git a/game/scenes/play.py b/game/scenes/play.py index 717c8fe..6814704 100644 --- a/game/scenes/play.py +++ b/game/scenes/play.py @@ -9,12 +9,14 @@ from game.entities.player import Player from game.entities.slime import Slime from game.scenes.scene import Scene +from game.states.game_states import GameStates class PlayScene(Scene): def __init__(self, gsm): super().__init__(gsm) + self.__reload_ticks = pygame.time.get_ticks() # Pause self.__pressed_pause = False @@ -37,37 +39,24 @@ def __init__(self, gsm): self.__bullets_group = Group() # Player - self.__player = Player(settings.SCREEN_WIDTH / 2 - 32, settings.SCREEN_HEIGHT / 2 - 32, 64, 64, 6) + self.__player = Player(settings.SCREEN_WIDTH / 2 - 32, + settings.SCREEN_HEIGHT / 2 - 32, + 64, 64, + speed=6, + health=5) self.__player_group.add(self.__player) # Font - font_path = str(settings.fonts_res / 'Arcade Classic.ttf') + font_path = str(settings.fonts_res / 'dpcomic.ttf') self.__health_font = pygame.font.Font(font_path, 32) - health_text = "HP %d" % self.__player.get_health() + health_text = "HP: %d" % self.__player.get_health() self.__player_health = self.__health_font.render(health_text, True, (180, 200, 210)) - # Slimes - slime_size = 48 - self.__slimes_count = 50 - for i in range(self.__slimes_count): - pos_choices = 'top', 'down', 'left', 'right' - pos_choice = random.choice(pos_choices) - rand_x = 0 - rand_y = 0 - if pos_choice == 'top': - rand_x = random.randint(-slime_size, settings.SCREEN_WIDTH - slime_size) - rand_y = -slime_size - elif pos_choice == 'down': - rand_x = random.randint(-slime_size, settings.SCREEN_WIDTH - slime_size) - rand_y = slime_size + settings.SCREEN_HEIGHT - elif pos_choice == 'left': - rand_x = -slime_size - rand_y = random.randint(-slime_size, settings.SCREEN_HEIGHT + slime_size) - elif pos_choice == 'right': - rand_x = slime_size + settings.SCREEN_WIDTH - rand_y = random.randint(-slime_size, settings.SCREEN_HEIGHT + slime_size) - slime = Slime(rand_x, rand_y, slime_size, slime_size, 2) - self.__enemies_group.add(slime) + # Game + self.__wave = 1 + self.__slimes_count = 3 + self.__last_slimes_count = self.__slimes_count + self.generate_slimes(self.__slimes_count) def draw(self, screen): self.__enemies_group.draw(screen) @@ -86,27 +75,38 @@ def update(self, dt): if not self.__pause: # Update Groups for enemy in self.__enemies_group.sprites(): + enemy.move_to_player(self.__player.get_pos()) for other_enemy in self.__enemies_group.sprites(): if enemy.colliderect(other_enemy.get_rect()): - enemy.throw(other_enemy.get_pos()) + enemy.throw(other_enemy.get_pos(), 1) + + for bullet in self.__bullets_group.sprites(): + if bullet.colliderect(enemy.get_rect()): + enemy.take_damage(self.__player.get_center(), bullet.get_damage()) + bullet.kill() # Check collides between slimes and player if self.__player.colliderect(enemy.get_rect()): self.__player.take_damage(enemy.get_pos(), 1) - for bullet in self.__bullets_group.sprites(): - if bullet.colliderect(enemy.get_rect()): - sub_health - self.__bullets_group.update(dt) self.__player_group.update(dt) self.__enemies_group.update(dt) - # Draw player health points - health_text = "HP %d" % self.__player.get_health() + # Update player health points + player_hp = self.__player.get_health() + health_text = f"HP: {player_hp}" self.__player_health = self.__health_font.render(health_text, True, (180, 200, 210)) + + self.__slimes_count = len(self.__enemies_group.sprites()) + if self.__slimes_count <= 0: + self.generate_slimes(self.__last_slimes_count + 3) + self.__wave += 1 + + if player_hp <= 0: + self._gsm.set_game_over_scene(self.__wave) else: # Pause Effect if self.__pause_effect_alpha <= 128: @@ -133,5 +133,41 @@ def handle_events(self, events): player_y = self.__player.get_center()[1] mouse_pos = pygame.mouse.get_pos() direction = (player_x - mouse_pos[0], player_y - mouse_pos[1]) - bullet = Bullet(player_x, player_y, 16, (255, 0, 0), direction, speed=10) + bullet = Bullet(player_x, player_y, 16, direction, speed=10, damage=1) + self.__bullets_group.add(bullet) + + mouse_pressed = pygame.mouse.get_pressed() + if mouse_pressed[0]: + reload_time = (pygame.time.get_ticks() - self.__reload_ticks) / 1000 + if reload_time > 0.1: + self.__reload_ticks = pygame.time.get_ticks() + player_x = self.__player.get_center()[0] + player_y = self.__player.get_center()[1] + mouse_pos = pygame.mouse.get_pos() + direction = (player_x - mouse_pos[0], player_y - mouse_pos[1]) + bullet = Bullet(player_x, player_y, 16, direction, speed=10, damage=1) self.__bullets_group.add(bullet) + + def generate_slimes(self, count): + slime_size = 48 + self.__slimes_count = count + self.__last_slimes_count = count + pos_choices = 'top', 'down', 'left', 'right' + pos_choice = random.choice(pos_choices) + for i in range(self.__slimes_count): + rand_x = 0 + rand_y = 0 + if pos_choice == 'top': + rand_x = random.randint(-slime_size, settings.SCREEN_WIDTH - slime_size) + rand_y = -slime_size + elif pos_choice == 'down': + rand_x = random.randint(-slime_size, settings.SCREEN_WIDTH - slime_size) + rand_y = slime_size + settings.SCREEN_HEIGHT + elif pos_choice == 'left': + rand_x = -slime_size + rand_y = random.randint(-slime_size, settings.SCREEN_HEIGHT + slime_size) + elif pos_choice == 'right': + rand_x = slime_size + settings.SCREEN_WIDTH + rand_y = random.randint(-slime_size, settings.SCREEN_HEIGHT + slime_size) + slime = Slime(rand_x, rand_y, slime_size, slime_size, 2, health=3) + self.__enemies_group.add(slime) diff --git a/game/states/game_manager.py b/game/states/game_states_manager.py similarity index 88% rename from game/states/game_manager.py rename to game/states/game_states_manager.py index 1f83625..442f022 100644 --- a/game/states/game_manager.py +++ b/game/states/game_states_manager.py @@ -26,6 +26,10 @@ def set_state(self, state: GameStates): def get_state(self): return self.__state + def set_game_over_scene(self, waves): + self.__state = GameStates.GAME_OVER + self.__scene = GameOverScene(self, waves) + def __get_scene(self, state: GameStates): if state == GameStates.MAIN_MENU: self.__state = GameStates.MAIN_MENU @@ -33,6 +37,3 @@ def __get_scene(self, state: GameStates): elif state == GameStates.PLAY: self.__state = GameStates.PLAY return PlayScene(self) - elif state == GameStates.GAME_OVER: - self.__state = GameStates.GAME_OVER - return GameOverScene(self) diff --git a/res/fonts/dpcomic.ttf b/res/fonts/dpcomic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a0776ae7e99e4d51100b765576c103875b7c7059 GIT binary patch literal 8940 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