import random
from core.ecs import Component
[docs]
class CameraComponent(Component):
DECAY_LINEAR = "linear"
DECAY_EXPONENTIAL = "exponential"
def __init__(
self,
active: bool = True,
zoom: float = 1.0,
rotation: float = 0.0,
viewport_x: float = 0.0,
viewport_y: float = 0.0,
viewport_width: float = 1.0,
viewport_height: float = 1.0,
priority: int = 0,
follow_target_id: str = "",
follow_rotation: bool = True
):
self.entity = None
self.active = bool(active)
self.zoom = max(0.01, float(zoom))
self.rotation = float(rotation)
self.viewport_x = float(viewport_x)
self.viewport_y = float(viewport_y)
self.viewport_width = max(0.0, float(viewport_width))
self.viewport_height = max(0.0, float(viewport_height))
self.priority = int(priority)
self.follow_target_id = str(follow_target_id or "")
self.follow_rotation = bool(follow_rotation)
# Shake state
self._shake_intensity: float = 0.0
self._shake_duration: float = 0.0
self._shake_elapsed: float = 0.0
self._shake_decay: str = self.DECAY_LINEAR
self._shake_offset_x: float = 0.0
self._shake_offset_y: float = 0.0
[docs]
def shake(self, intensity: float = 5.0, duration: float = 0.3,
decay: str = "linear"):
"""Start a camera shake effect.
Args:
intensity: Maximum pixel offset per axis.
duration: How long the shake lasts in seconds.
decay: Decay curve — ``"linear"`` or ``"exponential"``.
"""
self._shake_intensity = max(0.0, float(intensity))
self._shake_duration = max(0.0, float(duration))
self._shake_elapsed = 0.0
self._shake_decay = decay if decay in (self.DECAY_LINEAR, self.DECAY_EXPONENTIAL) else self.DECAY_LINEAR
[docs]
def update_shake(self, dt: float):
"""Advance the shake timer and compute the current offset.
Called by the render system each frame."""
if self._shake_duration <= 0.0 or self._shake_elapsed >= self._shake_duration:
self._shake_offset_x = 0.0
self._shake_offset_y = 0.0
return
self._shake_elapsed += dt
t = min(self._shake_elapsed / self._shake_duration, 1.0)
if self._shake_decay == self.DECAY_EXPONENTIAL:
factor = (1.0 - t) ** 2
else:
factor = 1.0 - t
magnitude = self._shake_intensity * factor
self._shake_offset_x = random.uniform(-magnitude, magnitude)
self._shake_offset_y = random.uniform(-magnitude, magnitude)
@property
def shake_offset(self) -> tuple[float, float]:
"""Current (x, y) shake offset in pixels."""
return (self._shake_offset_x, self._shake_offset_y)
@property
def is_shaking(self) -> bool:
return self._shake_elapsed < self._shake_duration and self._shake_duration > 0.0