- Add Camera class with modes: vertical, horizontal, omni, floating - Refactor scroll.py and demo to use Camera abstraction - Add vis_offset for horizontal scrolling support - Add camera_x to EffectContext for effects - Add pygame window resize handling - Add HUD effect plugin for demo mode - Add --demo flag to run demo mode - Add tests for Camera and vis_offset
110 lines
3.2 KiB
Python
110 lines
3.2 KiB
Python
"""
|
|
Camera system for viewport scrolling.
|
|
|
|
Provides abstraction for camera motion in different modes:
|
|
- Vertical: traditional upward scroll
|
|
- Horizontal: left/right movement
|
|
- Omni: combination of both
|
|
- Floating: sinusoidal/bobbing motion
|
|
"""
|
|
|
|
import math
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
|
|
|
|
class CameraMode(Enum):
|
|
VERTICAL = auto()
|
|
HORIZONTAL = auto()
|
|
OMNI = auto()
|
|
FLOATING = auto()
|
|
|
|
|
|
@dataclass
|
|
class Camera:
|
|
"""Camera for viewport scrolling.
|
|
|
|
Attributes:
|
|
x: Current horizontal offset (positive = scroll left)
|
|
y: Current vertical offset (positive = scroll up)
|
|
mode: Current camera mode
|
|
speed: Base scroll speed
|
|
custom_update: Optional custom update function
|
|
"""
|
|
|
|
x: int = 0
|
|
y: int = 0
|
|
mode: CameraMode = CameraMode.VERTICAL
|
|
speed: float = 1.0
|
|
custom_update: Callable[["Camera", float], None] | None = None
|
|
_time: float = field(default=0.0, repr=False)
|
|
|
|
def update(self, dt: float) -> None:
|
|
"""Update camera position based on mode.
|
|
|
|
Args:
|
|
dt: Delta time in seconds
|
|
"""
|
|
self._time += dt
|
|
|
|
if self.custom_update:
|
|
self.custom_update(self, dt)
|
|
return
|
|
|
|
if self.mode == CameraMode.VERTICAL:
|
|
self._update_vertical(dt)
|
|
elif self.mode == CameraMode.HORIZONTAL:
|
|
self._update_horizontal(dt)
|
|
elif self.mode == CameraMode.OMNI:
|
|
self._update_omni(dt)
|
|
elif self.mode == CameraMode.FLOATING:
|
|
self._update_floating(dt)
|
|
|
|
def _update_vertical(self, dt: float) -> None:
|
|
self.y += int(self.speed * dt * 60)
|
|
|
|
def _update_horizontal(self, dt: float) -> None:
|
|
self.x += int(self.speed * dt * 60)
|
|
|
|
def _update_omni(self, dt: float) -> None:
|
|
speed = self.speed * dt * 60
|
|
self.y += int(speed)
|
|
self.x += int(speed * 0.5)
|
|
|
|
def _update_floating(self, dt: float) -> None:
|
|
base = self.speed * 30
|
|
self.y = int(math.sin(self._time * 2) * base)
|
|
self.x = int(math.cos(self._time * 1.5) * base * 0.5)
|
|
|
|
def reset(self) -> None:
|
|
"""Reset camera position."""
|
|
self.x = 0
|
|
self.y = 0
|
|
self._time = 0.0
|
|
|
|
@classmethod
|
|
def vertical(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create a vertical scrolling camera."""
|
|
return cls(mode=CameraMode.VERTICAL, speed=speed)
|
|
|
|
@classmethod
|
|
def horizontal(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create a horizontal scrolling camera."""
|
|
return cls(mode=CameraMode.HORIZONTAL, speed=speed)
|
|
|
|
@classmethod
|
|
def omni(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create an omnidirectional scrolling camera."""
|
|
return cls(mode=CameraMode.OMNI, speed=speed)
|
|
|
|
@classmethod
|
|
def floating(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create a floating/bobbing camera."""
|
|
return cls(mode=CameraMode.FLOATING, speed=speed)
|
|
|
|
@classmethod
|
|
def custom(cls, update_fn: Callable[["Camera", float], None]) -> "Camera":
|
|
"""Create a camera with custom update function."""
|
|
return cls(custom_update=update_fn)
|