Ninja-Jump-and-run/scripts/entities.py

348 lines
12 KiB
Python
Raw Normal View History

2026-02-06 17:51:57 +00:00
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
2026-04-24 16:45:31 +00:00
from scripts.tilemap import Tilemap
2026-03-04 15:50:44 +00:00
@dataclass
class Collisions:
top: bool = False
right: bool = False
bottom: bool = False
left: bool = False
2026-04-24 16:45:31 +00:00
class PhysicsEntity:
def __init__(
self, game, e_type: str, pos: tuple[float, float], size: tuple[int, int]
):
2026-02-06 17:51:57 +00:00
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
2026-03-04 15:50:44 +00:00
# Animationen
self.action: str = ""
self.flip: bool = False
self.animation_offset: tuple[int, int] = (-3, -3)
self.animation: Animation
2026-04-24 16:45:31 +00:00
self.set_action("idle")
self.last_movement = [0, 0]
2026-04-24 16:45:31 +00:00
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]),
2026-04-24 16:45:31 +00:00
movement[1] + self.velocity[1],
)
2026-02-06 17:51:57 +00:00
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
2026-02-06 17:51:57 +00:00
self.game.isJumping = False
if frame_movement[0] < 0:
entity_rect.left = recto.right
self.collisions.left = True
2026-02-06 17:51:57 +00:00
self.game.isJumping = False
self.pos[0] = entity_rect.x
2026-04-24 16:45:31 +00:00
2026-02-06 17:51:57 +00:00
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
2026-02-06 17:51:57 +00:00
self.game.isJumping = False
if frame_movement[1] < 0:
entity_rect.top = rectolino.bottom
self.collisions.top = True
2026-02-06 17:51:57 +00:00
self.pos[1] = entity_rect.y
2026-04-24 16:45:31 +00:00
2026-03-04 15:50:44 +00:00
if movement[0] > 0:
self.flip = False
if movement[0] < 0:
self.flip = True
self.last_movement = movement
2026-02-06 17:51:57 +00:00
if self.collisions.bottom or self.collisions.top:
2026-04-24 16:45:31 +00:00
self.velocity[1] = 0
2026-02-06 17:51:57 +00:00
self.velocity[1] = min(5, self.velocity[1] + 0.1)
2026-03-04 15:50:44 +00:00
self.animation.update()
def render(self, surface: pygame.Surface, offset: tuple[int, int] = (0, 0)) -> None:
2026-04-24 16:45:31 +00:00
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:
2026-03-04 15:50:44 +00:00
return pygame.Rect(self.pos[0], self.pos[1], self.size[0], self.size[1])
2026-04-24 16:45:31 +00:00
def set_action(self, action: str) -> None:
2026-03-04 15:50:44 +00:00
if action != self.action:
self.action = action
image = self.e_type + "/" + self.action
self.animation = self.game.assets[image].copy()
2026-06-12 14:43:28 +00:00
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:
2026-06-12 14:43:28 +00:00
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)
2026-03-04 15:50:44 +00:00
2026-06-12 14:43:28 +00:00
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]),
)
2026-06-12 14:43:28 +00:00
2026-03-04 15:50:44 +00:00
class Player(PhysicsEntity):
def __init__(self, game, pos: tuple[float, float], size: tuple[int, int]):
2026-03-04 15:50:44 +00:00
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
2026-03-04 15:50:44 +00:00
def update(self, tilemap: Tilemap, movement: tuple[float, float] = (0, 0)) -> None:
2026-03-04 15:50:44 +00:00
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:
2026-03-04 15:50:44 +00:00
self.air_time = 0
self.jumps = 1
2026-03-04 15:50:44 +00:00
# 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,
]
2026-06-12 14:43:28 +00:00
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)
2026-06-12 14:43:28 +00:00
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)
2026-06-10 16:12:24 +00:00
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)
2026-04-24 16:45:31 +00:00
def dash(self) -> None:
if not self.dashing:
if self.flip:
self.dashing = -60
else:
self.dashing = 60
2026-04-24 16:45:31 +00:00
def render(self, surface, offset=(0, 0)):
if abs(self.dashing) <= 50:
2026-06-12 14:43:28 +00:00
super().render(surface, offset)