diff --git a/bullet.py b/bullet.py index c13f5c9..12b7bea 100644 --- a/bullet.py +++ b/bullet.py @@ -3,21 +3,28 @@ from config import SCREEN_WIDTH, SCREEN_HEIGHT, BULLET_SIZE, BULLET_SPEED + + class Bullet: - def __init__(self, x, y, target_x, target_y,color=(0, 0, 0)): + def __init__(self, x, y, target_x, target_y,color=(0, 0, 0),shooter=None): self.rect = pygame.Rect(x, y, BULLET_SIZE, BULLET_SIZE) angle = math.atan2(target_y - y, target_x - x) self.dx = math.cos(angle) * BULLET_SPEED self.dy = math.sin(angle) * BULLET_SPEED self.color = color + self.shooter = shooter + def update(self): self.rect.x += self.dx self.rect.y += self.dy + def draw(self, screen): pygame.draw.rect(screen,self.color, self.rect) + def is_off_screen(self): return (self.rect.x < 0 or self.rect.x > SCREEN_WIDTH or - self.rect.y < 0 or self.rect.y > SCREEN_HEIGHT) \ No newline at end of file + self.rect.y < 0 or self.rect.y > SCREEN_HEIGHT) + diff --git a/game.py b/game.py index f0dc673..32d95a5 100644 --- a/game.py +++ b/game.py @@ -1,6 +1,9 @@ """Game module""" + import pygame +import random + from bullet import Bullet from enums import KeyType @@ -8,15 +11,20 @@ from player import Player from npc import NPC + from config import GAME_FPS + + class AbstractGame: """Abstract game ancestor""" + def __init__(self, **kwargs): self.input_manager = InputManager() + self.screen = kwargs["screen"] self.clock = pygame.time.Clock() self.running = True @@ -24,12 +32,37 @@ def __init__(self, **kwargs): self.players = {} self.npcs = [] # todo DICT by id???? + self.bullets = [] self.player_bullets = [] self.npc_bullets = [] + self.npc_last_shot_times = {} self.npc_shoot_cooldown = 500 + self.score = 0 + self.last_spawn_time = pygame.time.get_ticks() + self.spawn_interval_range = (3000, 5000) + + + def spawn_random_npc(self): + npc_type = random.choices( + ["easy", "medium", "hard"], + weights=[70, 25, 5], + k=1 + )[0] + x = random.randint(50, self.screen.get_width() - 50) + y = random.randint(50, self.screen.get_height() - 50) + self.npcs.append(NPC(x, y, npc_type=npc_type)) + + + def try_spawn_npc(self): + now = pygame.time.get_ticks() + if now - self.last_spawn_time > self.next_spawn_interval: + self.spawn_random_npc() + self.last_spawn_time = now + # Nastav nový random interval pro další spawn + self.next_spawn_interval = random.randint(*self.spawn_interval_range) def try_npc_shoot(self, npc): @@ -39,12 +72,40 @@ def try_npc_shoot(self, npc): if now - last_shot >= self.npc_shoot_cooldown: target = npc.get_shot_target(self.players.values()) if target: - bullet = Bullet(npc.rect.centerx, npc.rect.centery, - target.rect.centerx, target.rect.centery, - color=(255, 0, 0)) + bullet = Bullet( + npc.rect.centerx, npc.rect.centery, + target.rect.centerx, target.rect.centery, + color=(255, 0, 0), + shooter=npc # tady přidáš referenci na střílejícího NPC + ) self.npc_bullets.append(bullet) self.npc_last_shot_times[npc_id] = now + + def check_bullet_collisions(self): + # NPC střely na hráče + for bullet in self.npc_bullets[:]: + for npc in self.npcs: + if bullet.shooter == npc: + for player in self.players.values(): + if bullet.rect.colliderect(player.rect): + player.health -= npc.damage + self.npc_bullets.remove(bullet) + break + break + + + for bullet in self.player_bullets[:]: + for npc in self.npcs[:]: + if bullet.rect.colliderect(npc.rect): + npc.health -= 10 + if npc.health <= 0: + self.npcs.remove(npc) + self.score += npc.score + self.player_bullets.remove(bullet) + break + + def handle_events(self): """Handles events and interruptions""" for event in pygame.event.get(): @@ -52,52 +113,75 @@ def handle_events(self): print("Pressed quitting!") self.running = False + def handle_key_events(self): """Handles key presses""" raise NotImplementedError + + def update_players(self, **kwargs): """Handles players updates""" + for player in self.players.values(): + inputs = self.input_manager.get_inputs(player.uid) + player.update(inputs = inputs, **kwargs) + if KeyType.SHOOT.name in inputs: bullet = player.shoot(self.npcs) if bullet is not None: self.player_bullets.append(bullet) + self.input_manager.clear_inputs(player.uid) + + def update_npcs(self, **kwargs): """Handles npcs updates""" + for npc in self.npcs: npc.update(players = self.players, **kwargs) self.try_npc_shoot(npc) + + def render_all(self): """Draws itself""" self.screen.fill((0, 0, 0)) + for player in self.players.values(): player.draw(self.screen) + for npc in self.npcs: npc.draw(self.screen) + for bullet in self.player_bullets: bullet.draw(self.screen) + for bullet in self.npc_bullets: bullet.draw(self.screen) + + font = pygame.font.SysFont(None, 30) + score_surface = font.render(f"Score: {self.score}", True, (255, 255, 255)) + self.screen.blit(score_surface, (10, 10)) + + def update_bullets(self): """Update bullets position""" for bullet_list in [self.player_bullets, self.npc_bullets]: @@ -106,84 +190,136 @@ def update_bullets(self): if bullet.is_off_screen(): bullet_list.remove(bullet) + def check_game_end(self): + alive_players = [p for p in self.players.values() if p.health > 0] + if not alive_players: + self.running = False + if self.score >= 200: + self.display_end_message("You win!") + else: + self.display_end_message("You lost.") + + def display_end_message(self, message): + font = pygame.font.SysFont(None, 60) + text = font.render(message, True, (255, 255, 255)) + rect = text.get_rect(center=(self.screen.get_width() // 2, self.screen.get_height() // 2)) + self.screen.fill((0, 0, 0)) + self.screen.blit(text, rect) + pygame.display.flip() + pygame.time.wait(3000) + def run(self): """Running loop""" + self.next_spawn_interval = random.randint(*self.spawn_interval_range) while self.running: self.handle_events() self.handle_key_events() + + self.try_spawn_npc() + + self.update_players() self.update_npcs() self.update_bullets() + self.render_all() pygame.display.flip() self.clock.tick(self.tick) + self.check_bullet_collisions() + self.check_game_end() + print("Closing game ....") + + class SingleGame(AbstractGame): """Single player game""" + PLAYER1 = "Player1" + def __init__(self, **kwargs): super().__init__(**kwargs) + pl1 = Player(self.PLAYER1) # todo some better init pl1.set_coords(20,20) self.players[self.PLAYER1] = pl1 + self.input_manager.add_keymap(self.PLAYER1, PLAYER_KEYMAPS["wasd"]) - self.npcs.append(NPC(400, 400)) + + self.spawn_random_npc() + def handle_key_events(self): """Handles key presses""" + keys = pygame.key.get_pressed() + for key in PLAYER_KEYMAPS["wasd"].keys(): if keys[key]: self.input_manager.add_input(self.PLAYER1, key) + + class CoopGame(AbstractGame): """Single player game""" + PLAYER1 = "Player1" PLAYER2 = "Player2" + def __init__(self, **kwargs): super().__init__(**kwargs) + pl1 = Player(self.PLAYER1) pl1.set_coords(20,20) self.players[self.PLAYER1] = pl1 + self.input_manager.add_keymap(self.PLAYER1, PLAYER_KEYMAPS["wasd"]) + pl2 = Player(self.PLAYER2) # experimental player 2 pl2.set_coords(100, 20) pl2.color = (0,0,255) self.players[self.PLAYER2] = pl2 + self.input_manager.add_keymap(self.PLAYER2, PLAYER_KEYMAPS["arrows"]) + self.npcs.append(NPC(400, 400)) + + def handle_key_events(self): """Handles key presses""" + keys = pygame.key.get_pressed() + for key in PLAYER_KEYMAPS["wasd"].keys(): if keys[key]: self.input_manager.add_input(self.PLAYER1, key) + for key in PLAYER_KEYMAPS["arrows"].keys(): if keys[key]: self.input_manager.add_input(self.PLAYER2, key) + diff --git a/npc.py b/npc.py index 948b93f..ffe508b 100644 --- a/npc.py +++ b/npc.py @@ -2,19 +2,43 @@ import math import pygame + from config import NPC_SPEED + class NPC: """NPC common class""" - def __init__(self, x, y): + + + TYPES = { + "easy": {"health": 50, "damage": 5, "color": (100, 255, 100), "score": 10}, + "medium": {"health": 100, "damage": 10, "color": (255, 255, 0), "score": 25}, + "hard": {"health": 150, "damage": 20, "color": (255, 100, 100), "score": 50}, + } + + + def __init__(self, x, y,npc_type="medium"): self.rect = pygame.Rect(x, y, 20, 20) + self.type = npc_type + npc_config = self.TYPES[self.type] + self.score = npc_config["score"] + self.health = npc_config["health"] + self.max_health = npc_config["health"] + self.damage = npc_config["damage"] + self.color = npc_config["color"] + + + + def update(self, **kwargs): """Updates self position according to players""" nearest_player = self.find_closest_player(self, kwargs["players"].values()) + + if self.rect.x < nearest_player.rect.x: self.rect.x += NPC_SPEED elif self.rect.x > nearest_player.rect.x: @@ -24,28 +48,48 @@ def update(self, **kwargs): elif self.rect.y > nearest_player.rect.y: self.rect.y -= NPC_SPEED + def get_shot_target(self, players): return self.find_closest_player(self, players) + def draw_lifebar(self, screen): + bar_width = self.rect.width + bar_heigth = 5 + fill = (self.health / self.max_health) * bar_width + + + pygame.draw.rect(screen, (255, 0, 0), (self.rect.x, self.rect.y - 10, bar_width, bar_heigth)) + pygame.draw.rect(screen, (0, 255, 0), (self.rect.x, self.rect.y - 10, fill, bar_heigth)) + + + + def draw(self, screen): """Draws itself""" pygame.draw.rect(screen, (255, 0, 0), self.rect) + self.draw_lifebar(screen) + + def distance(self, rect1, rect2): """Helper function for distance computing""" return math.hypot(rect1.x - rect2.x, rect1.y - rect2.y) + def find_closest_player(self, npc, players): """Function for NPC to find which player to chase, not optimized""" closest = None min_dist = float('inf') + for player in players: dist = self.distance(npc.rect, player.rect) if dist < min_dist: min_dist = dist closest = player + return closest + diff --git a/player.py b/player.py index 314c7b9..302ffb1 100644 --- a/player.py +++ b/player.py @@ -2,16 +2,20 @@ import math import pygame + from config import PLAYER_WIDTH, PLAYER_HEIGHT, PLAYER_SPEED, SCREEN_WIDTH, SCREEN_HEIGHT from enums import KeyType, Facing from bullet import Bullet + movement_keys = {KeyType.LEFT.name, KeyType.RIGHT.name, KeyType.UP.name, KeyType.DOWN.name} + class Player: """Player class init""" def __init__(self, uid): + self.uid = uid self.rect = pygame.Rect(0, 0, PLAYER_WIDTH, PLAYER_HEIGHT) self.color = (0, 255, 0) @@ -19,27 +23,38 @@ def __init__(self, uid): self.speed = PLAYER_SPEED self.is_moving = False self.facing = Facing.RIGHT + self.health = 100 + self.max_health = 100 + + self.shoot_cooldown = 250 self.last_shot_time = pygame.time.get_ticks() + + def set_coords(self, x, y): """Coords setter""" self.rect = pygame.Rect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT) + def set_name(self, name): """Name setter""" self.name = name + def update(self, **kwargs): """Update self position from move intention""" + inp = kwargs["inputs"] + self.is_moving = any(key in inp for key in movement_keys) + if KeyType.UP.name in inp: self.rect.y -= self.speed self.facing = Facing.UP @@ -47,6 +62,7 @@ def update(self, **kwargs): self.rect.y += self.speed self.facing = Facing.DOWN + if KeyType.LEFT.name in inp: self.rect.x -= self.speed self.facing = Facing.LEFT @@ -54,44 +70,65 @@ def update(self, **kwargs): self.rect.x += self.speed self.facing = Facing.RIGHT + # overflow corrections - todo možná udělat přes kolize s okrajem! self.rect.x = max(self.rect.x, 0) self.rect.y = max(self.rect.y, 0) self.rect.x = min(self.rect.x, SCREEN_WIDTH - PLAYER_WIDTH) self.rect.y = min(self.rect.y, SCREEN_HEIGHT - PLAYER_HEIGHT) + def distance(self, rect1, rect2): """Helper function for distance computing""" return math.hypot(rect1.x - rect2.x, rect1.y - rect2.y) + def find_closest_npc(self, npcs): closest = None min_dist = float('inf') + for npc in npcs: dist = self.distance(self.rect, npc.rect) if dist < min_dist: min_dist = dist closest = npc + return closest + + def shoot(self, npcs): """Try to shoot, does not fire in cooldown""" + now = pygame.time.get_ticks() if now - self.last_shot_time >= self.shoot_cooldown: target = self.find_closest_npc(npcs) if target is None: return None + target_pos = (target.rect.centerx, target.rect.centery) bullet = Bullet(self.rect.centerx, self.rect.centery, *target_pos, color=self.color) self.last_shot_time = now return bullet + def draw_lifebar(self, screen): + bar_width = self.rect.width + bar_heigth = 5 + fill = (self.health / self.max_health) * bar_width + + + pygame.draw.rect(screen, (255, 0, 0), (self.rect.x, self.rect.y - 10, bar_width, bar_heigth)) + pygame.draw.rect(screen, (0, 255, 0), (self.rect.x, self.rect.y - 10, fill, bar_heigth)) + + def draw(self, screen): """Draws itself""" pygame.draw.rect(screen, self.color, self.rect) + self.draw_lifebar(screen) +