import pygame import math import random from dataclasses import dataclass from scripts.animation import Animation from scripts.particle import Particle from scripts.spark import Spark from scripts.tilemap import Tilemap @dataclass class Collisions: top: bool = False right: bool = False bottom: bool = False left: bool = False class PhysicsEntity: def __init__( self, game, e_type: str, pos: tuple[float, float], size: tuple[int, int] ): self.game = game self.e_type: str = e_type self.pos: list[float] = list(pos) self.size: tuple[int, int] = size self.velocity: list[float] = [0, 0] self.collisions: Collisions = Collisions() self.speed: int = 3 # Animationen self.action: str = "" self.flip: bool = False self.animation_offset: tuple[int, int] = (-3, -3) self.animation: Animation self.set_action("idle") self.last_movement = [0, 0] def update(self, tilemap: Tilemap, movement: tuple[float, float] = (0, 0)) -> None: self.collisions = Collisions() frame_movement: tuple[float, float] = ( (movement[0] + self.velocity[0]), movement[1] + self.velocity[1], ) self.pos[0] += frame_movement[0] entity_rect = self.rect() for recto in tilemap.physics_rects_around(self.pos): if entity_rect.colliderect(recto): if frame_movement[0] > 0: entity_rect.right = recto.left self.collisions.right = True self.game.isJumping = False if frame_movement[0] < 0: entity_rect.left = recto.right self.collisions.left = True self.game.isJumping = False self.pos[0] = entity_rect.x self.pos[1] += frame_movement[1] entity_rect = self.rect() for rectolino in tilemap.physics_rects_around(self.pos): if entity_rect.colliderect(rectolino): if frame_movement[1] > 0: entity_rect.bottom = rectolino.top self.collisions.bottom = True self.game.isJumping = False if frame_movement[1] < 0: entity_rect.top = rectolino.bottom self.collisions.top = True self.pos[1] = entity_rect.y if movement[0] > 0: self.flip = False if movement[0] < 0: self.flip = True self.last_movement = movement if self.collisions.bottom or self.collisions.top: self.velocity[1] = 0 self.velocity[1] = min(5, self.velocity[1] + 0.1) self.animation.update() def render(self, surface: pygame.Surface, offset: tuple[int, int] = (0, 0)) -> None: surface.blit( pygame.transform.flip(self.animation.img(), self.flip, False), ( self.pos[0] - offset[0] + self.animation_offset[0], self.pos[1] - offset[1] + self.animation_offset[1], ), ) def rect(self) -> pygame.Rect: return pygame.Rect(self.pos[0], self.pos[1], self.size[0], self.size[1]) def set_action(self, action: str) -> None: if action != self.action: self.action = action image = self.e_type + "/" + self.action self.animation = self.game.assets[image].copy() class Enemy(PhysicsEntity): def __init__(self, game, pos: tuple[float, float], size: tuple[int, int]): super().__init__(game, "enemy", pos, size) self.walking = 0 def update(self, tilemap, movement: tuple[float, float] = (0, 0)): if self.walking: if tilemap.solid_check( (self.rect().centerx + (-7 if self.flip else 7), self.pos[1] + 23) ): if self.collisions.right or self.collisions.left: self.flip = not self.flip else: movement = (movement[0] - 0.5 if self.flip else 0.5, movement[1]) else: self.flip = not self.flip self.walking = max(0, self.walking - 1) if not self.walking: distance = ( self.game.player.pos[0] - self.pos[0], self.game.player.pos[1] - self.pos[1], ) if abs(distance[1]) < 16: if self.flip and distance[0] < 0: self.game.projectiles.append( [[self.rect().centerx - 7, self.rect().centery], -1.5, 0] ) for _ in range(4): self.game.sparks.append( Spark( self.game.projectiles[-1][0], random.random() - 0.5 + math.pi, random.random(), ) ) if not self.flip and distance[0] > 0: self.game.projectiles.append( [[self.rect().centerx + 7, self.rect().centery], 1.5, 0] ) for _ in range(4): self.game.sparks.append( Spark( self.game.projectiles[-1][0], random.random() - 0.5, random.random(), ) ) elif random.random() < 0.02: self.walking = random.randint(30, 120) super().update(tilemap, movement) if movement[0] != 0: self.set_action("run") else: self.set_action("idle") if abs(self.game.player.dashing) >= 50: if self.rect().colliderect(self.game.player.rect()): self.game.screenshake = max(16, self.game.screenshake) for _ in range(30): angle = random.random() * math.pi * 2 speed = random.random() * 5 self.game.sparks.append(Spark(self.rect().center, angle, 2 + random.random())) self.game.particles.append(Particle(self.game, 'particle', self.rect().center, velocity=[math.cos(angle + math.pi) * speed * 0.5, math.sin(angle + math.pi) * speed * 0.5], frame=random.randint(0, 7))) self.game.sparks.append(Spark(self.rect().center, 0, 5 + random.random())) self.game.sparks.append(Spark(self.rect().center, math.pi, 5 + random.random())) return True def render(self, surface: pygame.Surface, offset: tuple = (0, 0)): super().render(surface, offset) if self.flip: surface.blit( pygame.transform.flip(self.game.assets["gun"], True, False), ( self.rect().centerx - 4 - self.game.assets["gun"].get_width() - offset[0], self.rect().centery - offset[1], ), ) else: surface.blit( self.game.assets["gun"], (self.rect().centerx + 4 - offset[0], self.rect().centery - offset[1]), ) class Player(PhysicsEntity): def __init__(self, game, pos: tuple[float, float], size: tuple[int, int]): super().__init__(game, "player", pos, size) self.air_time: int = 0 # Neu! Zählt, wie lange wir fallen/springen self.jumps: int = 0 self.wall_slide: bool = False self.dashing: int = 0 def update(self, tilemap: Tilemap, movement: tuple[float, float] = (0, 0)) -> None: super().update(tilemap, movement) self.air_time += 1 if self.air_time > 120 and not self.wall_slide: if not self.game.dead: self.game.screenshake = max(16, self.game.screenshake) self.game.dead += 1 if self.collisions.bottom: self.air_time = 0 self.jumps = 1 # Wenn wir länger als 4 Frames in der Luft sind -> Springen! self.wall_slide = False if (self.collisions.right or self.collisions.left) and self.air_time > 4: self.wall_slide = True self.air_time = 5 self.velocity[1] = min(self.velocity[1], 0.5) if self.collisions.right: self.flip = False else: self.flip = True self.set_action("wall_slide") if not self.wall_slide: if self.air_time > 4: self.set_action("jump") # Wenn wir nicht in der Luft sind, aber uns bewegen -> Rennen! elif movement[0] != 0: self.set_action("run") # Sonst -> Rumstehen! else: self.set_action("idle") if abs(self.dashing) in {60, 50}: for _ in range(20): particle_angle = random.random() * math.pi * 2 particle_speed = random.random() * 0.5 + 0.5 particle_velocity = [ math.cos(particle_angle) * particle_speed, math.sin(particle_angle) * particle_speed, ] particle_pos = self.rect().center particle_startframe = random.randint(0, 7) self.game.particles.append( Particle( self.game, "particle", particle_pos, particle_velocity, particle_startframe, ) ) if self.dashing > 0: self.dashing = max(0, self.dashing - 1) # 60 → 59 → ... → 0 if self.dashing < 0: self.dashing = min(0, self.dashing + 1) # -60 → -59 → ... → 0 if abs(self.dashing) > 50: self.velocity[0] = abs(self.dashing) / self.dashing * 8 if abs(self.dashing) == 51: self.velocity[0] *= 0.1 particle_velocity = [ abs(self.dashing) / self.dashing * random.random() * 3, 0, ] self.game.particles.append( Particle( self.game, "particle", self.rect().center, particle_velocity, random.randint(0, 7), ) ) if self.velocity[0] > 0: self.velocity[0] = max(self.velocity[0] - 0.1, 0) else: self.velocity[0] = min(self.velocity[0] + 0.1, 0) def jump(self) -> None | bool: if self.wall_slide: if self.flip and self.last_movement[0] < 0: self.velocity[0] = 3.5 self.velocity[1] = -2.5 self.air_time = 5 self.jumps = max(0, self.jumps - 1) elif not self.flip and self.last_movement[0] > 0: self.velocity[0] = -3.5 self.velocity[1] = -2.5 self.air_time = 5 self.jumps = max(0, self.jumps - 1) elif self.jumps > 0: # and not self.dashing self.velocity[1] = -3 self.jumps -= 1 self.air_time = 5 return True def boost(self, boosting: bool = False): # if boosting: # self.velocity[1] -= 5 # boost_particle = Particle(self.game, "boost", (self.pos[0] + self.size[0]//2, self.pos[1] + self.size[1]//2)) # self.game.particles.append(boost_particle) self.velocity[1] -= 5 boost_particle = Particle( self.game, "boost", (self.pos[0] + self.size[0] // 2, self.pos[1] + self.size[1] // 2), ) self.game.particles.append(boost_particle) def dash(self) -> None: if not self.dashing: if self.flip: self.dashing = -60 else: self.dashing = 60 def render(self, surface, offset=(0, 0)): if abs(self.dashing) <= 50: super().render(surface, offset)