diff --git a/game/code/data.py b/game/code/data.py new file mode 100644 index 0000000..fe1efb0 --- /dev/null +++ b/game/code/data.py @@ -0,0 +1,36 @@ +from csv import reader +from os import walk +import pygame + +# ОСНОВНЫЕ НАСТРОЙКИ +WIDTH = 1080 +HEIGTH = 720 +FPS = 60 +TILESIZE = 64 +HITBOX = {'player': -26, 'stone': -10, 'invisible': 0} + +weapon_data = {'sword': {'cooldown': 10, 'damage': 1}} # набор оружия +mobs_data = {'ninja': {'health': 100, 'exp': 250, 'damage': 6, # тип и характеристики моба + 'attack_type': 'leaf_attack', 'attack_sound': '../data/audio/hit.wav', + 'speed': 3, 'resistance': 3, 'attack_radius': 50, 'notice_radius': 10000}} + + +def import_csv_layout(path): # ЗАГРУЗКА ФАЙЛОВ CSV КАРТЫ + terrain_map = [] + with open(path) as level_map: + layout = reader(level_map, delimiter=',') + for row in layout: + terrain_map.append(list(row)) + return terrain_map + + +def import_folder(path): # ОБРАБОТКА КАРТИНОК + surface_list = [] + + for i, j, img_files in walk(path): + for image in img_files: + full_path = path + '/' + image + image_surf = pygame.image.load(full_path).convert_alpha() + surface_list.append(image_surf) + + return surface_list diff --git a/game/code/enemy.py b/game/code/enemy.py new file mode 100644 index 0000000..7772598 --- /dev/null +++ b/game/code/enemy.py @@ -0,0 +1,124 @@ +from data import * +from entity import Entity + + +class Enemy(Entity): + def __init__(self, monster_name, pos, groups, obstacle_sprites, damage_player, add_exp): + super().__init__(groups) + self.sprite_type = 'enemy' + + # стартовый спрайт + self.import_graphics(monster_name) + self.status = 'idle' + self.image = self.animations[self.status][self.frame_index] + + # положение + self.rect = self.image.get_rect(topleft=pos) + self.hitbox = self.rect.inflate(0, -10) + self.obstacle_sprites = obstacle_sprites + + # характеристики + self.monster_name = monster_name + monster_info = mobs_data[self.monster_name] + self.health = monster_info['health'] + self.exp = monster_info['exp'] + self.speed = monster_info['speed'] + self.attack_damage = monster_info['damage'] + self.resistance = monster_info['resistance'] + self.attack_radius = monster_info['attack_radius'] + self.notice_radius = monster_info['notice_radius'] + + # параметры + self.can_attack = True + self.attack_time = None + self.attack_cooldown = 400 + self.damage_player = damage_player + self.add_exp = add_exp + + # звуки + self.death_sound = pygame.mixer.Sound('../data/audio/death.wav') + self.hit_sound = pygame.mixer.Sound('../data/audio/hit.wav') + self.attack_sound = pygame.mixer.Sound(monster_info['attack_sound']) + self.death_sound.set_volume(0.6) + self.hit_sound.set_volume(0.6) + self.attack_sound.set_volume(0.6) + + def import_graphics(self, name): + self.animations = {'idle': [], 'move': [], 'attack': []} + main_path = f'../data/textures/mobs/{name}/' + for animation in self.animations.keys(): + self.animations[animation] = import_folder(main_path + animation) + + def get_player_distance_direction(self, player): + enemy_vec = pygame.math.Vector2(self.rect.center) + player_vec = pygame.math.Vector2(player.rect.center) + distance = (player_vec - enemy_vec).magnitude() + + if distance > 0: + direction = (player_vec - enemy_vec).normalize() + else: + direction = pygame.math.Vector2() + + return (distance, direction) + + def get_status(self, player): + distance = self.get_player_distance_direction(player)[0] + + if distance <= self.attack_radius and self.can_attack: + if self.status != 'attack': + self.frame_index = 0 + self.status = 'attack' + elif distance <= self.notice_radius: + self.status = 'move' + else: + self.status = 'idle' + + def actions(self, player): + if self.status == 'attack': + self.attack_time = pygame.time.get_ticks() + self.damage_player(self.attack_damage) + self.attack_sound.play() + elif self.status == 'move': + self.direction = self.get_player_distance_direction(player)[1] + else: + self.direction = pygame.math.Vector2() + + def animate(self): + animation = self.animations[self.status] + + self.frame_index += self.animation_speed + if self.frame_index >= len(animation): + if self.status == 'attack': + self.can_attack = False + self.frame_index = 0 + + self.image = animation[int(self.frame_index)] + self.rect = self.image.get_rect(center=self.hitbox.center) + + def cooldowns(self): + current_time = pygame.time.get_ticks() + if not self.can_attack: + if current_time - self.attack_time >= self.attack_cooldown: + self.can_attack = True + + def get_damage(self, player): + self.hit_sound.play() + self.direction = self.get_player_distance_direction(player)[1] + self.health -= player.get_full_weapon_damage() + self.hit_time = pygame.time.get_ticks() + + def check_death(self): + if self.health <= 0: + self.kill() + self.add_exp(self.exp) + self.death_sound.play() + + def update(self): + self.move(self.speed) + self.animate() + self.cooldowns() + self.check_death() + + def enemy_update(self, player): + self.get_status(player) + self.actions(player) diff --git a/game/code/entity.py b/game/code/entity.py new file mode 100644 index 0000000..fa2cb86 --- /dev/null +++ b/game/code/entity.py @@ -0,0 +1,44 @@ +import pygame +from math import sin + + +class Entity(pygame.sprite.Sprite): + def __init__(self, groups): + super().__init__(groups) + self.frame_index = 0 + self.animation_speed = 0.15 + self.direction = pygame.math.Vector2() + + def move(self, speed): + if self.direction.magnitude() != 0: + self.direction = self.direction.normalize() + + self.hitbox.x += self.direction.x * speed + self.collision('horizontal') + self.hitbox.y += self.direction.y * speed + self.collision('vertical') + self.rect.center = self.hitbox.center + + def collision(self, direction): + if direction == 'horizontal': + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if self.direction.x > 0: # moving right + self.hitbox.right = sprite.hitbox.left + if self.direction.x < 0: # moving left + self.hitbox.left = sprite.hitbox.right + + if direction == 'vertical': + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if self.direction.y > 0: # moving down + self.hitbox.bottom = sprite.hitbox.top + if self.direction.y < 0: # moving up + self.hitbox.top = sprite.hitbox.bottom + + def wave_value(self): + value = sin(pygame.time.get_ticks()) + if value >= 0: + return 255 + else: + return 0 diff --git a/game/code/level.py b/game/code/level.py new file mode 100644 index 0000000..b0adbbc --- /dev/null +++ b/game/code/level.py @@ -0,0 +1,187 @@ +from random import choice + +from data import * +from player import Player +from enemy import Enemy + + +class Level: + def __init__(self): + self.display_surface = pygame.display.get_surface() + self.ui = UI() + + # группы основных спрайтов видимых и нет + self.visible_sprites = Camera() + self.obstacle_sprites = pygame.sprite.Group() + + # группа спрайтов способных разрушиться + self.current_attack = None + self.attack_sprites = pygame.sprite.Group() + self.attackable_sprites = pygame.sprite.Group() + + # генерация карты + self.create_map() + + def create_map(self): + layouts = { + 'barryer': import_csv_layout('../data/map/барьеры.csv'), + 'stone': import_csv_layout('../data/map/камни.csv'), + 'entities': import_csv_layout('../data/map/мобы.csv') + } + graphics = {'stone': import_folder('../data/textures/stone')} + + for style, layout in layouts.items(): + for row_index, row in enumerate(layout): + for col_index, col in enumerate(row): + if col != '-1': + x = col_index * TILESIZE + y = row_index * TILESIZE + if style == 'barryer': + Tile((x, y), [self.obstacle_sprites], 'invisible') + if style == 'stone': + random_grass_image = choice(graphics['stone']) + Tile((x, y), [self.visible_sprites, self.obstacle_sprites, self.attackable_sprites], + 'stone', random_grass_image) + if style == 'entities': + if col == '2': + self.player = Player((x, y), [self.visible_sprites], self.obstacle_sprites, + self.destroy_attack, self.create_attack) + else: + Enemy("ninja", (x, y), [self.visible_sprites, self.attackable_sprites], + self.obstacle_sprites, self.damage_player, self.add_exp) + + def create_attack(self): # создание атаки и спрайта оружия + self.current_attack = Weapon(self.player, [self.visible_sprites, self.attack_sprites]) + + def destroy_attack(self): # разрушение разрушаемых объектов + if self.current_attack: + self.current_attack.kill() + self.current_attack = None + + def player_attack(self): # удары по другим объектам + if self.attack_sprites: + for attack_sprite in self.attack_sprites: + collision_sprites = pygame.sprite.spritecollide(attack_sprite, self.attackable_sprites, False) + if collision_sprites: + for target_sprite in collision_sprites: + if target_sprite.sprite_type == 'stone': # камни разрушаются с одного удара + target_sprite.kill() + else: # мобы получают урон + target_sprite.get_damage(self.player) + + def damage_player(self, amount): + self.player.health -= amount + self.player.hurt_time = pygame.time.get_ticks() + + def add_exp(self, amount): + self.player.exp += amount + + def run(self): + self.visible_sprites.custom_draw(self.player) + self.ui.display(self.player) + + self.visible_sprites.update() + self.visible_sprites.enemy_update(self.player) + self.player_attack() + + +# КАМЕРА СЛЕДЯЩАЯ ЗА ПЕРСОНАЖЕМ +class Camera(pygame.sprite.Group): + def __init__(self): + super().__init__() + self.display_surface = pygame.display.get_surface() + self.half_width = self.display_surface.get_size()[0] // 2 + self.half_height = self.display_surface.get_size()[1] // 2 + self.offset = pygame.math.Vector2() + + # отрисовка основной карты + self.floor_surf = pygame.image.load('../data/map/map.png').convert() + self.floor_rect = self.floor_surf.get_rect(topleft=(0, 0)) + + def custom_draw(self, player): + self.offset.x = player.rect.centerx - self.half_width # ПОЛУЧЕНИЕ ПОЗИЦИИ ИГРОКА + self.offset.y = player.rect.centery - self.half_height + + floor_offset_pos = self.floor_rect.topleft - self.offset # ОТРИСОВКА ОСНОВНОЙ КАРТЫ ПОД ИГРОКОМ + self.display_surface.blit(self.floor_surf, floor_offset_pos) + + for sprite in sorted(self.sprites(), key=lambda sprite: sprite.rect.centery): + offset_pos = sprite.rect.topleft - self.offset + self.display_surface.blit(sprite.image, offset_pos) + + def enemy_update(self, player): # ОТРИСОВКА МОБОВ + enemy_sprites = [] + for sprite in self.sprites(): + if hasattr(sprite, 'sprite_type') and sprite.sprite_type == 'enemy': + enemy_sprites.append(sprite) + + for enemy in enemy_sprites: + enemy.enemy_update(player) + + +# КЛАСС ОТРИСОВКИ ПЛИТОК +class Tile(pygame.sprite.Sprite): + def __init__(self, pos, groups, sprite_type, surface=pygame.Surface((TILESIZE, TILESIZE))): + super().__init__(groups) + self.sprite_type = sprite_type + y_offset = HITBOX[sprite_type] + self.image = surface + + if sprite_type == 'object': + self.rect = self.image.get_rect(topleft=(pos[0], pos[1] - TILESIZE)) + else: + self.rect = self.image.get_rect(topleft=pos) + + self.hitbox = self.rect.inflate(0, y_offset) + + +# КЛАСС ОРУЖИЯ +class Weapon(pygame.sprite.Sprite): + def __init__(self, player, groups): + super().__init__(groups) + self.sprite_type = 'weapon' + self.image = pygame.image.load(f'../data/textures/sword/{player.status.split("_")[0]}.png').convert_alpha() + + # отрисовка оружия по отношению к герою + if player.status == 'right': + self.rect = self.image.get_rect(midleft=player.rect.midright + pygame.math.Vector2(-3, 16)) + elif player.status == 'left': + self.rect = self.image.get_rect(midright=player.rect.midleft + pygame.math.Vector2(3, 16)) + elif player.status == 'down': + self.rect = self.image.get_rect(midtop=player.rect.midbottom + pygame.math.Vector2(-10, 0)) + else: + self.rect = self.image.get_rect(midbottom=player.rect.midtop + pygame.math.Vector2(-10, 20)) + + +# КЛАСС ИНТРЕФЕЙСА +class UI: + def __init__(self): + self.screen = pygame.display.get_surface() + self.font = pygame.font.SysFont("arial", 24) # размер и формат чисел опыта + + def health_bar(self, player): + pygame.draw.rect(self.screen, '#222222', pygame.Rect(10, 10, 120, 30)) + + # перевод кол-ва здоровья в полоску + ratio = player.health / player.stats['health'] + current_width = pygame.Rect(10, 10, 120, 30).width * ratio + current_rect = pygame.Rect(10, 10, 120, 30).copy() + current_rect.width = current_width + + pygame.draw.rect(self.screen, 'red', current_rect) + pygame.draw.rect(self.screen, 'black', pygame.Rect(10, 10, 120, 30), 3) + + def show_exp(self, exp): + text_surf = self.font.render(str(int(exp)), False, 'white') + x = self.screen.get_size()[0] - 20 # место ячейки опыта + y = self.screen.get_size()[1] - 1040 + text_rect = text_surf.get_rect(bottomright=(x, y)) + + pygame.draw.rect(self.screen, '#222222', text_rect.inflate(20, 20)) # отрисовка опыта и серого фона + self.screen.blit(text_surf, text_rect) + + pygame.draw.rect(self.screen, 'black', text_rect.inflate(20, 20), 3) # отрисовка чёрной рамки + + def display(self, player): + self.health_bar(player) + self.show_exp(player.exp) diff --git a/game/code/main.py b/game/code/main.py new file mode 100644 index 0000000..a015e73 --- /dev/null +++ b/game/code/main.py @@ -0,0 +1,39 @@ +import sys + +from data import * +from level import Level + + +class Game: + def __init__(self): + pygame.init() + self.screen = pygame.display.set_mode((WIDTH, HEIGTH)) + pygame.display.set_caption('Dormitorium') + self.clock = pygame.time.Clock() + + self.level = Level() + + main_sound = pygame.mixer.Sound('../data/audio/main.wav') + main_sound.set_volume(0.3) + main_sound.play(loops=-1) + + def run(self): + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + pygame.quit() + sys.exit() + + self.screen.fill('black') + self.level.run() + pygame.display.update() + self.clock.tick(FPS) + + +if __name__ == '__main__': + game = Game() + game.run() diff --git a/game/code/player.py b/game/code/player.py new file mode 100644 index 0000000..83e2385 --- /dev/null +++ b/game/code/player.py @@ -0,0 +1,128 @@ +from data import * +from entity import Entity + + +class Player(Entity): + def __init__(self, pos, groups, obstacle_sprites, destroy_attack, create_attack): + super().__init__(groups) + self.image = pygame.image.load('../data/textures/player/player.png').convert_alpha() + self.rect = self.image.get_rect(topleft=pos) + self.hitbox = self.rect.inflate(-6, HITBOX['player']) + + # стартовое положение спрайта + self.import_player_assets() + self.status = 'down' + + # параметры героя + self.attacking = False + self.attack_cooldown = 400 + self.attack_time = None + self.obstacle_sprites = obstacle_sprites + + # оружик героя + self.destroy_attack = destroy_attack + self.weapon_index = 0 + self.create_attack = create_attack + self.weapon = list(weapon_data.keys())[self.weapon_index] + self.can_switch_weapon = True + self.weapon_switch_time = None + self.switch_duration_cooldown = 200 + + # характеристики героя + self.stats = {'health': 300, 'attack': 10, 'speed': 5} + self.max_stats = {'health': 300, 'attack': 20, 'speed': 10} + self.health = self.stats['health'] + self.speed = self.stats['speed'] + self.exp = 0 + + # import a sound + self.weapon_attack_sound = pygame.mixer.Sound('../data/audio/sword.wav') + self.weapon_attack_sound.set_volume(0.4) + + def import_player_assets(self): # получение всех картинок героя + self.animations = {'up': [], 'down': [], 'left': [], 'right': [], + 'right_stand': [], 'left_stand': [], 'up_stand': [], 'down_stand': [], + 'right_attack': [], 'left_attack': [], 'up_attack': [], 'down_attack': []} + + for animation in self.animations.keys(): + full_path = '../data/textures/player/' + animation + self.animations[animation] = import_folder(full_path) + + def input(self): # кнопки + if not self.attacking: + keys = pygame.key.get_pressed() + mouse = pygame.mouse.get_pressed() + + if keys[pygame.K_w]: + self.direction.y = -1 + self.status = 'up' + elif keys[pygame.K_s]: + self.direction.y = 1 + self.status = 'down' + else: + self.direction.y = 0 + + if keys[pygame.K_d]: + self.direction.x = 1 + self.status = 'right' + elif keys[pygame.K_a]: + self.direction.x = -1 + self.status = 'left' + else: + self.direction.x = 0 + + if mouse[0]: + self.attacking = True + self.attack_time = pygame.time.get_ticks() + self.create_attack() + self.weapon_attack_sound.play() + + def get_status(self): # положение героя на поле + if self.direction.x == 0 and self.direction.y == 0: + if not 'stand' in self.status and not 'attack' in self.status: + self.status = self.status + '_stand' + + if self.attacking: + self.direction.x = 0 + self.direction.y = 0 + if not 'attack' in self.status: + if 'stand' in self.status: + self.status = self.status.replace('_stand', '_attack') + else: + self.status = self.status + '_attack' + else: + if 'attack' in self.status: + self.status = self.status.replace('_attack', '') + + def cooldowns(self): # таймер востановления удара + current_time = pygame.time.get_ticks() + + if self.attacking: + if current_time - self.attack_time >= self.attack_cooldown + weapon_data[self.weapon]['cooldown']: + self.attacking = False + self.destroy_attack() + + if not self.can_switch_weapon: + if current_time - self.weapon_switch_time >= self.switch_duration_cooldown: + self.can_switch_weapon = True + + def animate(self): # анимирование героя + animation = self.animations[self.status] + self.frame_index += self.animation_speed + if self.frame_index >= len(animation): + self.frame_index = 0 + + self.image = animation[int(self.frame_index)] + self.rect = self.image.get_rect(center=self.hitbox.center) + + def get_full_weapon_damage(self): # нанесение врагу урона + base_damage = self.stats['attack'] + weapon_damage = weapon_data[self.weapon]['damage'] + return base_damage + weapon_damage + + def update(self): + self.input() + self.cooldowns() + self.get_status() + self.animate() + self.move(self.stats['speed'])