import os import sys import random import math import pygame from scripts.utils import load_image, load_images, Animation from scripts.entities import Player, Enemy, PhysicsEntity, Particle, Spark from scripts.tilemap import Tilemap from scripts.clouds import Clouds class Game: TRANSITION_DURATION = 30 DEATH_DELAY = 10 def __init__(self): pygame.init() pygame.display.set_caption('ninja game') self.screen = pygame.display.set_mode((640, 480)) self.display = pygame.Surface((320, 240), pygame.SRCALPHA) self.display_2 = pygame.Surface((320, 240)) self.clock = pygame.time.Clock() self.movement = [False, False] def load_asset(path): if os.path.isdir(path): return load_images(path) else: return load_image(path) try: self.assets = { 'decor': load_asset('tiles/decor'), 'grass': load_asset('tiles/grass'), 'large_decor': load_asset('tiles/large_decor'), 'stone': load_asset('tiles/stone'), 'player': load_asset('entities/player.png'), 'background': load_asset('background.png'), 'clouds': load_asset('clouds'), 'enemy/idle': Animation(load_images('entities/enemy/idle'), img_dur=6), 'enemy/run': Animation(load_images('entities/enemy/run'), img_dur=4), 'player/idle': Animation(load_images('entities/player/idle'), img_dur=6), 'player/run': Animation(load_images('entities/player/run'), img_dur=4), 'player/jump': Animation(load_images('entities/player/jump')), 'player/slide': Animation(load_images('entities/player/slide')), 'player/wall_slide': Animation(load_images('entities/player/wall_slide')), 'particle/leaf': Animation(load_images('particles/leaf'), img_dur=20, loop=False), 'particle/particle': Animation(load_images('particles/particle'), img_dur=6, loop=False), 'gun': load_asset('gun.png'), 'projectile': load_asset('projectile.png'), 'key': load_asset('entities/key.png'), 'door': load_asset('entities/door.png'), } except FileNotFoundError as e: print(f"Error loading assets: {e}") sys.exit() try: self.sfx = { 'jump': pygame.mixer.Sound('data/sfx/jump.wav'), 'dash': pygame.mixer.Sound('data/sfx/dash.wav'), 'hit': pygame.mixer.Sound('data/sfx/hit.wav'), 'shoot': pygame.mixer.Sound('data/sfx/shoot.wav'), 'ambience': pygame.mixer.Sound('data/sfx/ambience.wav'), } except FileNotFoundError as e: print(f"Error loading sound effects: {e}") sys.exit() self.sfx['ambience'].set_volume(0.2) self.sfx['shoot'].set_volume(0.4) self.sfx['hit'].set_volume(0.8) self.sfx['dash'].set_volume(0.3) self.sfx['jump'].set_volume(0.7) self.clouds = Clouds(self.assets['clouds'], count=16) self.player = Player(self, (50, 50), (8, 15)) self.tilemap = Tilemap(self, tile_size=16) self.level = 1 self.score = 0 self.load_level(self.level) self.screenshake = 0 self.dead = 0 self.transition = -30 def load_level(self, map_id): try: self.tilemap.load('data/maps/' + str(map_id) + '.json') except FileNotFoundError as e: print(f"Error loading level: {e}") sys.exit() self.leaf_spawners = [] for tree in self.tilemap.extract([('large_decor', 2)], keep=True): self.leaf_spawners.append(pygame.Rect(4 + tree['pos'][0], 4 + tree['pos'][1], 23, 13)) self.enemies = [] for spawner in self.tilemap.extract([('spawners', 0), ('spawners', 1)]): if spawner['variant'] == 0: self.player.pos = spawner['pos'] self.player.air_time = 0 else: self.enemies.append(Enemy(self, spawner['pos'], (8, 15))) self.keys = [] self.doors = [] for spawner in self.tilemap.extract([('spawners', 2)]): if spawner['variant'] == 0: self.keys.append(PhysicsEntity(self, spawner['pos'], (16, 16), 'key')) else: self.doors.append(PhysicsEntity(self, spawner['pos'], (16, 16), 'door')) self.projectiles = [] self.particles = [] self.sparks = [] self.scroll = [0, 0] self.dead = 0 self.transition = -30 def run(self): try: pygame.mixer.music.load('data/music.wav') except FileNotFoundError as e: print(f"Error loading music: {e}") sys.exit() pygame.mixer.music.set_volume(0.5) pygame.mixer.music.play(-1) self.sfx['ambience'].play(-1) running = True while running: self.display.fill((0, 0, 0, 0)) self.display_2.blit(self.assets['background'], (0, 0)) self.screenshake = max(0, self.screenshake - 1) if not len(self.enemies): self.transition += 1 if self.transition > self.TRANSITION_DURATION: self.level = min(self.level + 1, len(os.listdir('data/maps')) - 1) self.load_level(self.level) if self.transition < 0: self.transition += 1 if self.dead: self.dead += 1 if self.dead >= self.DEATH_DELAY: self.transition = min(self.TRANSITION_DURATION, self.transition + 1) if self.dead > 40: self.load_level(self.level) self.scroll[0] += (self.player.rect().centerx - self.display.get_width() / 2 - self.scroll[0]) / 30 self.scroll[1] += (self.player.rect().centery - self.display.get_height() / 2 - self.scroll[1]) / 30 render_scroll = (int(self.scroll[0]), int(self.scroll[1])) for rect in self.leaf_spawners: if random.random() * 49999 < rect.width * rect.height: pos = (rect.x + random.random() * rect.width, rect.y + random.random() * rect.height) self.particles.append(Particle(self, 'leaf', pos, velocity=[-0.1, 0.3], frame=random.randint(0, 20))) self.clouds.update() self.clouds.render(self.display_2, offset=render_scroll) self.tilemap.render(self.display, offset=render_scroll) for enemy in self.enemies.copy(): kill = enemy.update(self.tilemap, (0, 0)) enemy.render(self.display, offset=render_scroll) if kill: self.enemies.remove(enemy) if not self.dead: self.player.update(self.tilemap, (self.movement[1] - self.movement[0], 0)) self.player.render(self.display, offset=render_scroll) for projectile in self.projectiles.copy(): projectile[0][0] += projectile[1] projectile[2] += 1 img = self.assets['projectile'] self.display.blit(img, (projectile[0][0] - img.get_width() / 2 - render_scroll[0], projectile[0][1] - img.get_height() / 2 - render_scroll[1])) if self.tilemap.solid_check(projectile[0]): self.projectiles.remove(projectile) for i in range(4): self.sparks.append(Spark(projectile[0], random.random() - 0.5 + (math.pi if projectile[1] > 0 else 0), 2 + random.random())) elif projectile[2] > 360: self.projectiles.remove(projectile) elif abs(self.player.dashing) < 50: if self.player.rect().collidepoint(projectile[0]): self.projectiles.remove(projectile) self.dead += 1 self.sfx['hit'].play() self.screenshake = max(16, self.screenshake) for i in range(30): angle = random.random() * math.pi * 2 speed = random.random() * 5 self.sparks.append(Spark(self.player.rect().center, angle, 2 + random.random())) self.particles.append(Particle(self, 'particle', self.player.rect().center, velocity=[math.cos(angle + math.pi) * speed * 0.5, math.sin(angle + math.pi) * speed * 0.5], frame=random.randint(0, 7))) for spark in self.sparks.copy(): kill = spark.update() spark.render(self.display, offset=render_scroll) if kill: self.sparks.remove(spark) display_mask = pygame.mask.from_surface(self.display) display_silhouette = display_mask.to_surface(setcolor=(0, 0, 0, 180), unsetcolor=(0, 0, 0, 0)) for offset in [(-1, 0), (1, 0), (0, -1), (0, 1)]: self.display_2.blit(display_silhouette, offset) for particle in self.particles.copy(): kill = particle.update() particle.render(self.display, offset=render_scroll) if particle.type == 'leaf': particle.pos[0] += math.sin(particle.animation.frame * 0.035) * 0.3 if kill: self.particles.remove(particle) for key in self.keys.copy(): if self.player.rect().colliderect(key.rect()): self.keys.remove(key) self.player.key = True for door in self.doors.copy(): if self.player.rect().colliderect(door.rect()) and self.player.key: self.doors.remove(door) self.level += 1 self.load_level(self.level) for key in self.keys: key.render(self.display, offset=render_scroll) for door in self.doors: door.render(self.display, offset=render_scroll) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False pygame.quit() sys.exit() if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: self.movement[0] = True if event.key == pygame.K_RIGHT: self.movement[1] = True if event.key == pygame.K_UP: if self.player.jump(): self.sfx['jump'].play() if event.key == pygame.K_x: self.player.dash() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT: self.movement[0] = False if event.key == pygame.K_RIGHT: self.movement[1] = False if self.transition: transition_surf = pygame.Surface(self.display.get_size()) pygame.draw.circle(transition_surf, (255, 255, 255), (self.display.get_width() // 2, self.display.get_height() // 2), (self.TRANSITION_DURATION - abs(self.transition)) * 8) transition_surf.set_colorkey((255, 255, 255)) self.display.blit(transition_surf, (0, 0)) self.display_2.blit(self.display, (0, 0)) screenshake_offset = (random.random() * self.screenshake - self.screenshake / 2, random.random() * self.screenshake - self.screenshake / 2) self.screen.blit(pygame.transform.scale(self.display_2, self.screen.get_size()), screenshake_offset) pygame.display.update() self.clock.tick(60) if __name__ == '__main__': Game().run()