diff --git a/src/display/__init__.py b/src/display/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/game/display.py b/src/display/display.py similarity index 97% rename from src/game/display.py rename to src/display/display.py index 245fb47..6e4d919 100644 --- a/src/game/display.py +++ b/src/display/display.py @@ -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 @@ -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) diff --git a/src/entity/ball/ball.py b/src/entity/ball/ball.py index 0491c50..71b3541 100644 --- a/src/entity/ball/ball.py +++ b/src/entity/ball/ball.py @@ -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 @@ -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__( @@ -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 @@ -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)}" @@ -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, @@ -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 ) @@ -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: diff --git a/src/entity/ball/modifiers/__init__.py b/src/entity/ball/modifiers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/entity/ball/modifiers/angry_modifier.py b/src/entity/ball/modifiers/angry_modifier.py new file mode 100644 index 0000000..c693fb8 --- /dev/null +++ b/src/entity/ball/modifiers/angry_modifier.py @@ -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) diff --git a/src/entity/ball/modifiers/ball_modifier.py b/src/entity/ball/modifiers/ball_modifier.py new file mode 100644 index 0000000..c2b6b9f --- /dev/null +++ b/src/entity/ball/modifiers/ball_modifier.py @@ -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 diff --git a/src/entity/ball/modifiers/ball_modifiers.py b/src/entity/ball/modifiers/ball_modifiers.py new file mode 100644 index 0000000..7436f88 --- /dev/null +++ b/src/entity/ball/modifiers/ball_modifiers.py @@ -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) \ No newline at end of file diff --git a/src/entity/ball/modifiers/pulse_modifier.py b/src/entity/ball/modifiers/pulse_modifier.py new file mode 100644 index 0000000..03e9039 --- /dev/null +++ b/src/entity/ball/modifiers/pulse_modifier.py @@ -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 \ No newline at end of file diff --git a/src/entity/entity.py b/src/entity/entity.py index 4e23503..cb0a390 100644 --- a/src/entity/entity.py +++ b/src/entity/entity.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from src.game.display import Display +from src.display.display import Display class Entity(ABC): diff --git a/src/entity/wall.py b/src/entity/wall.py index 8e22735..b1934d5 100644 --- a/src/entity/wall.py +++ b/src/entity/wall.py @@ -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): diff --git a/src/faces/face_configuration.py b/src/faces/face_configuration.py index a2fb866..dd3ba2c 100644 --- a/src/faces/face_configuration.py +++ b/src/faces/face_configuration.py @@ -5,5 +5,5 @@ @dataclass(frozen=True) class FaceConfiguration: happy_path: Path - sad_path: Path + angry_path: Path diameter: int diff --git a/src/faces/loaded_face_configuration.py b/src/faces/loaded_face_configuration.py index cc7f270..018f93b 100644 --- a/src/faces/loaded_face_configuration.py +++ b/src/faces/loaded_face_configuration.py @@ -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: diff --git a/src/game/game.py b/src/game/game.py index 370dfb4..8a87fda 100644 --- a/src/game/game.py +++ b/src/game/game.py @@ -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 diff --git a/src/visuals/damage_number_effect.py b/src/visuals/damage_number_effect.py index b3ff651..b69df2a 100644 --- a/src/visuals/damage_number_effect.py +++ b/src/visuals/damage_number_effect.py @@ -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 diff --git a/src/visuals/face_implosion_effect.py b/src/visuals/face_implosion_effect.py index 3db51e1..8d034f0 100644 --- a/src/visuals/face_implosion_effect.py +++ b/src/visuals/face_implosion_effect.py @@ -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 diff --git a/src/visuals/halo_effect.py b/src/visuals/halo_effect.py index 4ec1db3..6a2e181 100644 --- a/src/visuals/halo_effect.py +++ b/src/visuals/halo_effect.py @@ -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 diff --git a/src/visuals/implosion_effect.py b/src/visuals/implosion_effect.py index 4892cc2..7aab7a9 100644 --- a/src/visuals/implosion_effect.py +++ b/src/visuals/implosion_effect.py @@ -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 diff --git a/src/visuals/visual_effect.py b/src/visuals/visual_effect.py index 52322b6..46e4829 100644 --- a/src/visuals/visual_effect.py +++ b/src/visuals/visual_effect.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from src.game.display import Display +from src.display.display import Display class VisualEffect(ABC): diff --git a/src/visuals/visual_effect_manager.py b/src/visuals/visual_effect_manager.py index 10c14cb..eee0b76 100644 --- a/src/visuals/visual_effect_manager.py +++ b/src/visuals/visual_effect_manager.py @@ -1,4 +1,4 @@ -from src.game.display import Display +from src.display.display import Display from src.visuals.visual_effect import VisualEffect