Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added src/display/__init__.py
Empty file.
6 changes: 4 additions & 2 deletions src/game/display.py → src/display/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def draw_circle(
temp_surface = pygame.Surface((diameter, diameter), pygame.SRCALPHA)

# Draw the circle at the center of the temporary surface
circle_color = (*color[:3], alpha)
circle_color = pygame.Color(color.r, color.g, color.b, alpha)
pygame.draw.circle(temp_surface, circle_color, (radius, radius), int(radius), width)

# Calculate the top-left position to blit the temp surface
Expand All @@ -51,10 +51,12 @@ def draw_image(
self,
image: pygame.Surface,
center: tuple[float, float],
angle_deg: float = 0
angle_deg: float = 0,
alpha: int = 255,
) -> None:
if angle_deg != 0:
image = pygame.transform.rotate(image, angle_deg)
image.set_alpha(alpha)
rect = image.get_rect(center=center)
self.sim_surface.blit(image, rect.topleft)

Expand Down
27 changes: 16 additions & 11 deletions src/entity/ball/ball.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
import pymunk

from src.entity.ball.ball_spawn_config import BallSpawnConfig
from src.entity.ball.modifiers.angry_modifier import AngryModifier
from src.entity.ball.modifiers.ball_modifiers import BallModifiers
from src.entity.ball.modifiers.pulse_modifier import PulseModifier
from src.entity.entity import Entity
from src.faces.loaded_face_configuration import LoadedFaceConfiguration
from src.game.display import Display
from src.display.display import Display
from src.visuals.damage_number_effect import DamageNumberEffect
from src.visuals.face_implosion_effect import FaceImplosionEffect
from src.visuals.halo_effect import HaloEffect
Expand All @@ -22,7 +25,6 @@ class Ball(Entity):
SPEEDUP_CHANCE = 0.1
VELOCITY_CAP = 2000

ANGRY_FACE_SECONDS_PER_DAMAGE = 0.1
CRIT_SECONDS = 1

def __init__(
Expand Down Expand Up @@ -58,13 +60,15 @@ def __init__(
self.faces = LoadedFaceConfiguration(self.prototype.faces) if self.prototype.faces else None

self.visual_effect_manager = visual_effect_manager
self.modifiers = BallModifiers()

@override
def update(self, dt: float) -> None:
if self.health <= 0:
return

self.hit_timer_seconds = max(0.0, self.hit_timer_seconds - dt)
self.modifiers.update(dt)

if random.random() < self.SPEEDUP_CHANCE and self.body.velocity.length < self.VELOCITY_CAP:
self.body.velocity *= 1 + self.SPEEDUP_RATE

Expand All @@ -75,13 +79,12 @@ def draw(self, display: Display) -> None:

pos = (self.body.position.x, self.body.position.y)

# Ball base
display.draw_circle(pos, self.radius, self.prototype.color)

# Face image (rotated)
alpha = self.modifiers.get_pulse_alpha()
if self.prototype.faces is not None:
angle_deg = -self.body.angle * 180 / math.pi
display.draw_image(self.get_current_face(), pos, angle_deg)
display.draw_image(self.get_current_face(), pos, angle_deg, alpha)
else:
display.draw_circle(pos, self.radius, self.prototype.color, alpha)

# Health text below the ball
health_text = f"{int(self.health)}"
Expand All @@ -94,9 +97,11 @@ def deal_damage(self, damage: int, is_crit: bool) -> None:

def receive_damage(self, damage: int, is_crit: bool) -> None:
if damage > 0:
self.hit_timer_seconds = damage * self.ANGRY_FACE_SECONDS_PER_DAMAGE
self.health = max(0, self.health - damage)

self.modifiers.add(AngryModifier(damage))
self.modifiers.add(PulseModifier())

self.visual_effect_manager.add(
DamageNumberEffect(
self,
Expand All @@ -120,7 +125,7 @@ def receive_damage(self, damage: int, is_crit: bool) -> None:
FaceImplosionEffect(
pos=(self.body.position.x, self.body.position.y),
angle_deg=-self.body.angle * 180 / math.pi,
face_surface=self.faces.sad_surface,
face_surface=self.faces.angry_surface,
initial_radius=self.radius + 10,
duration=0.5
)
Expand All @@ -138,7 +143,7 @@ def receive_damage(self, damage: int, is_crit: bool) -> None:

def get_current_face(self) -> pygame.Surface:
assert self.faces is not None
return self.faces.happy_surface if self.hit_timer_seconds == 0 else self.faces.sad_surface
return self.faces.angry_surface if self.modifiers.is_angry() else self.faces.happy_surface

@property
def name(self) -> str:
Expand Down
Empty file.
8 changes: 8 additions & 0 deletions src/entity/ball/modifiers/angry_modifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from src.entity.ball.modifiers.ball_modifier import BallModifier


class AngryModifier(BallModifier):
SECONDS_PER_DAMAGE = 0.1

def __init__(self, damage: int) -> None:
super().__init__(damage * self.SECONDS_PER_DAMAGE)
21 changes: 21 additions & 0 deletions src/entity/ball/modifiers/ball_modifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from src.entity.ball.ball import Ball


class BallModifier:
def __init__(self, duration: float) -> None:
self.duration = duration

def update(self, dt: float) -> None:
self.duration = max(0.0, self.duration - dt)

def is_active(self) -> bool:
return self.duration > 0

def apply(self, ball: Ball) -> None:
"""Override in subclasses to modify drawing or behavior."""
pass
25 changes: 25 additions & 0 deletions src/entity/ball/modifiers/ball_modifiers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from src.entity.ball.modifiers.angry_modifier import AngryModifier
from src.entity.ball.modifiers.ball_modifier import BallModifier
from src.entity.ball.modifiers.pulse_modifier import PulseModifier


class BallModifiers:
def __init__(self) -> None:
self.modifiers: list[BallModifier] = []

def update(self, dt: float) -> None:
for modifier in self.modifiers:
modifier.update(dt)
self.modifiers = [modifier for modifier in self.modifiers if modifier.is_active()]

def add(self, modifier: BallModifier) -> None:
self.modifiers.append(modifier)

def is_angry(self) -> bool:
return any(isinstance(modifier, AngryModifier) for modifier in self.modifiers)

def get_pulse_alpha(self) -> int:
for modifier in self.modifiers:
if isinstance(modifier, PulseModifier):
return modifier.get_alpha()
return 255 # Default alpha (fully opaque)
40 changes: 40 additions & 0 deletions src/entity/ball/modifiers/pulse_modifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import math
from src.entity.ball.modifiers.ball_modifier import BallModifier

class PulseModifier(BallModifier):
DURATION = 0.7
NUM_PULSES = 3

def __init__(
self,
duration: float = DURATION,
min_alpha: int = 120,
max_alpha: int = 255,
min_scale: float = 0.92,
max_scale: float = 1.08,
):
super().__init__(duration)
self.initial_duration = duration
self.min_alpha = min_alpha
self.max_alpha = max_alpha
self.min_scale = min_scale
self.max_scale = max_scale

def get_progress(self) -> float:
return 1.0 - (self.duration / self.initial_duration)

def get_alpha(self) -> int:
# Ease in-out: alpha dips in the middle, returns to max at the end
progress = self.get_progress()
# Use a bell-curve shape: dips at the middle, max at start/end
# alpha = min + (max - min) * |cos(pi * progress)|
pulse = abs(math.cos(math.pi * progress * self.NUM_PULSES))
return int(self.min_alpha + (self.max_alpha - self.min_alpha) * pulse)

def get_scale(self) -> float:
# Pop scale: starts big, shrinks, then returns to normal
progress = self.get_progress()
# Use a similar bell-curve shape, but inverted for scale
# scale = min_scale + (max_scale - min_scale) * sin(pi * progress)
pulse = math.sin(math.pi * progress)
return self.min_scale + (self.max_scale - self.min_scale) * pulse
2 changes: 1 addition & 1 deletion src/entity/entity.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod

from src.game.display import Display
from src.display.display import Display


class Entity(ABC):
Expand Down
2 changes: 1 addition & 1 deletion src/entity/wall.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pymunk
from src.entity.entity import Entity
from src.game.display import Display
from src.display.display import Display


class Wall(Entity):
Expand Down
2 changes: 1 addition & 1 deletion src/faces/face_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
@dataclass(frozen=True)
class FaceConfiguration:
happy_path: Path
sad_path: Path
angry_path: Path
diameter: int
2 changes: 1 addition & 1 deletion src/faces/loaded_face_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def __init__(
face_configuration: FaceConfiguration,
):
self.happy_surface = self.load_circular_image(face_configuration.happy_path, face_configuration.diameter)
self.sad_surface = self.load_circular_image(face_configuration.sad_path, face_configuration.diameter)
self.angry_surface = self.load_circular_image(face_configuration.angry_path, face_configuration.diameter)

@staticmethod
def load_circular_image(path: Path, diameter: int) -> pygame.Surface:
Expand Down
2 changes: 1 addition & 1 deletion src/game/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from src.entity.ball.ball import Ball
from src.entity.ball.ball_spawn_config import BallSpawnConfig
from src.entity.wall import Wall
from src.game.display import Display
from src.display.display import Display
from src.visuals.visual_effect_manager import VisualEffectManager


Expand Down
2 changes: 1 addition & 1 deletion src/visuals/damage_number_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pygame

from src.game.display import Display
from src.display.display import Display

if TYPE_CHECKING:
from src.entity.ball.ball import Ball
Expand Down
2 changes: 1 addition & 1 deletion src/visuals/face_implosion_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pygame

from src.game.display import Display
from src.display.display import Display
from src.visuals.visual_effect import VisualEffect


Expand Down
2 changes: 1 addition & 1 deletion src/visuals/halo_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pygame

from src.game.display import Display
from src.display.display import Display

if TYPE_CHECKING:
from src.entity.ball.ball import Ball
Expand Down
2 changes: 1 addition & 1 deletion src/visuals/implosion_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pygame

from src.game.display import Display
from src.display.display import Display
from src.visuals.visual_effect import VisualEffect


Expand Down
2 changes: 1 addition & 1 deletion src/visuals/visual_effect.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod

from src.game.display import Display
from src.display.display import Display


class VisualEffect(ABC):
Expand Down
2 changes: 1 addition & 1 deletion src/visuals/visual_effect_manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.game.display import Display
from src.display.display import Display
from src.visuals.visual_effect import VisualEffect


Expand Down