Space_recycling_game / src /backgrounds.py
Antoni09's picture
Upload game files
c02cd3b verified
Raw
History Blame Contribute Delete
10.4 kB
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable, Optional
import pygame
from src.assets import AssetLibrary
@dataclass(frozen=True)
class LayerSpec:
path_parts: tuple[str, ...]
speed_factor: float
alpha: int = 255
colorkey: tuple[int, int, int] | None = None
class ParallaxBackground:
"""
Parallax background using Foozle environment pack layers.
Source images are very wide strips (e.g., 5760x360). We scale each layer to
screen height and tile horizontally with a scrolling offset.
"""
def __init__(self, screen_size: tuple[int, int], assets: AssetLibrary):
self.w, self.h = screen_size
self.assets = assets
self.base_speed = 80.0 # px/sec; tuned for current prototype
self.offset = 0.0
self.layers: list[tuple[pygame.Surface, float]] = []
self._apply_layer_specs(list(self._default_layers()))
self.planet: Optional[pygame.Surface] = self._load_planet()
self.planet_pos = pygame.Vector2(self.w * 0.75, self.h * 0.25)
self.planet_speed = 10.0
self.tint_mult = (255, 255, 255)
self._tint_surface: Optional[pygame.Surface] = None
self.nebula: Optional[pygame.Surface] = None
self.nebula_factor: float = 0.10
def set_tint_mult(self, rgb: tuple[int, int, int]) -> None:
self.tint_mult = (int(rgb[0]), int(rgb[1]), int(rgb[2]))
if self.tint_mult == (255, 255, 255):
self._tint_surface = None
return
self._tint_surface = pygame.Surface((self.w, self.h))
self._tint_surface.fill(self.tint_mult)
def _default_layers(self) -> Iterable[LayerSpec]:
# Prefer Split up layers for flexibility.
base = (
"Foozle_2DS0015_Void_EnvironmentPack",
"Backgrounds",
"PNGs",
"Split up",
)
return [
LayerSpec(base + ("Starry background - Layer 01 - Solid colour.png",), 0.15),
LayerSpec(base + ("Starry background - Layer 02 - Shadows.png",), 0.35),
LayerSpec(base + ("Starry background - Layer 03 - Stars.png",), 0.70),
]
def _apply_layer_specs(self, specs: list[LayerSpec]) -> None:
self.layers = []
for spec in specs:
surf = self._load_layer(spec.path_parts)
if surf is None:
continue
if spec.colorkey is not None:
surf.set_colorkey(spec.colorkey)
if spec.alpha != 255:
surf.set_alpha(spec.alpha)
self.layers.append((surf, spec.speed_factor))
def set_layers(self, specs: Iterable[LayerSpec]) -> None:
self._apply_layer_specs(list(specs))
def _load_layer(self, parts: tuple[str, ...]) -> Optional[pygame.Surface]:
path = self.assets.resolve("environment_pack", *parts[1:]) if parts[0] == "Foozle_2DS0015_Void_EnvironmentPack" else self.assets.resolve(parts[0], *parts[1:])
if not path.exists():
return None
img = self.assets.load_image(path)
# Scale to screen height, preserve aspect ratio.
scale = self.h / img.get_height()
new_w = max(1, int(img.get_width() * scale))
scaled = pygame.transform.smoothscale(img, (new_w, self.h))
return scaled
def _load_planet(self) -> Optional[pygame.Surface]:
path = self.assets.resolve(
"environment_pack",
"Planets",
"PNGs",
"Earth-Like planet.png",
)
if not path.exists():
return None
img = self.assets.load_image(path)
target = 140
scale = target / max(1, img.get_width())
w = max(1, int(img.get_width() * scale))
h = max(1, int(img.get_height() * scale))
return pygame.transform.smoothscale(img, (w, h))
def set_planet_variant(self, variant: str, *, scale: float = 1.0) -> None:
if variant == "none":
self.planet = None
return
planet_file = "Earth-Like planet.png" if variant == "glow" else "Earth-Like planet - Without back glow.png"
path = self.assets.resolve("environment_pack", "Planets", "PNGs", planet_file)
if not path.exists():
self.planet = None
return
img = self.assets.load_image(path)
target = int(140 * max(0.5, float(scale)))
s = target / max(1, img.get_width())
w = max(1, int(img.get_width() * s))
h = max(1, int(img.get_height() * s))
self.planet = pygame.transform.smoothscale(img, (w, h))
self.planet_pos = pygame.Vector2(self.w * 0.75, self.h * 0.25)
def set_nebula(
self,
*,
seed: int | None,
colors: tuple[tuple[int, int, int], ...],
alpha: int = 80,
factor: float = 0.10,
) -> None:
self.nebula_factor = float(factor)
if seed is None or not colors:
self.nebula = None
return
import random
rng = random.Random(int(seed))
surf = pygame.Surface((self.w * 2, self.h), pygame.SRCALPHA)
surf.fill((0, 0, 0, 0))
for _ in range(30):
col = rng.choice(list(colors))
radius = rng.randint(90, 240)
x = rng.randint(-radius, surf.get_width() + radius)
y = rng.randint(-radius, self.h + radius)
a = rng.randint(max(20, int(alpha) - 40), min(150, int(alpha) + 35))
pygame.draw.circle(surf, (int(col[0]), int(col[1]), int(col[2]), a), (x, y), radius)
self.nebula = surf
def set_planet_from_layer(self, path_parts: tuple[str, ...], *, scale: float = 1.0) -> None:
try:
path = self.assets.resolve(path_parts[0], *path_parts[1:])
except Exception:
self.planet = None
return
if not path.exists():
self.planet = None
return
img = self.assets.load_image(path)
target = int(140 * max(0.5, float(scale)))
s = target / max(1, img.get_width())
w = max(1, int(img.get_width() * s))
h = max(1, int(img.get_height() * s))
self.planet = pygame.transform.smoothscale(img, (w, h))
self.planet_pos = pygame.Vector2(self.w * 0.75, self.h * 0.25)
def configure_theme(
self,
*,
shadows_variant: int = 0,
stars_variant: int = 0,
extras: tuple[str, ...] = (),
tint_mult: tuple[int, int, int] = (255, 255, 255),
planet_variant: str = "glow", # glow | noglow | none
planet_scale: float = 1.0,
nebula_seed: int | None = None,
nebula_colors: tuple[tuple[int, int, int], ...] = (),
nebula_alpha: int = 80,
) -> None:
base = ("Foozle_2DS0015_Void_EnvironmentPack", "Backgrounds", "PNGs", "Split up")
shadows_file = "Starry background - Layer 02 - Shadows.png" if int(shadows_variant) == 0 else "Starry background - Layer 02 - Shadows 2.png"
stars_file = "Starry background - Layer 03 - Stars.png" if int(stars_variant) == 0 else "Starry background - Layer 03 - Stars 2.png"
specs: list[LayerSpec] = [
LayerSpec(base + ("Starry background - Layer 01 - Solid colour.png",), 0.15),
LayerSpec(base + (shadows_file,), 0.35),
LayerSpec(base + (stars_file,), 0.70),
]
extra_map = {
"big_star": "Starry background - Layer X - Big Star.png",
"big_star_2": "Starry background - Layer X - Big Star 2.png",
"black_hole": "Starry background - Layer X -Black hole.png",
"rotary_star": "Starry background - Layer X -Rotary Star.png",
"rotary_star_2": "Starry background - Layer X -Rotary Star 2.png",
}
for key in extras:
name = extra_map.get(str(key))
if name:
# Extras should be subtle; keep low opacity to avoid dominating the scene.
specs.append(LayerSpec(base + (name,), 0.10, alpha=120))
self._apply_layer_specs(specs)
self.set_tint_mult(tint_mult)
# Planet
if planet_variant == "none":
self.planet = None
else:
planet_file = "Earth-Like planet.png" if planet_variant == "glow" else "Earth-Like planet - Without back glow.png"
path = self.assets.resolve("environment_pack", "Planets", "PNGs", planet_file)
if path.exists():
img = self.assets.load_image(path)
target = int(140 * max(0.5, float(planet_scale)))
scale = target / max(1, img.get_width())
w = max(1, int(img.get_width() * scale))
h = max(1, int(img.get_height() * scale))
self.planet = pygame.transform.smoothscale(img, (w, h))
else:
self.planet = None
self.planet_pos = pygame.Vector2(self.w * 0.75, self.h * 0.25)
# Nebula overlay (procedural), wide so it tiles.
self.set_nebula(seed=nebula_seed, colors=nebula_colors, alpha=nebula_alpha, factor=self.nebula_factor)
def update(self, dt: float):
self.offset += self.base_speed * dt
if self.offset > 1_000_000:
self.offset = 0.0
if self.planet is not None:
self.planet_pos.x -= self.planet_speed * dt
if self.planet_pos.x < -200:
self.planet_pos.x = self.w + 200
def draw(self, surface: pygame.Surface):
for layer, factor in self.layers:
self._draw_tiled(surface, layer, self.offset * factor)
if self.planet is not None:
rect = self.planet.get_rect(center=(int(self.planet_pos.x), int(self.planet_pos.y)))
surface.blit(self.planet, rect)
if self.nebula is not None:
self._draw_tiled(surface, self.nebula, self.offset * self.nebula_factor)
if self._tint_surface is not None:
surface.blit(self._tint_surface, (0, 0), special_flags=pygame.BLEND_RGB_MULT)
def _draw_tiled(self, surface: pygame.Surface, img: pygame.Surface, x_offset: float):
w = img.get_width()
# Scroll left-to-right: subtract offset for left movement.
x = -int(x_offset) % w
x -= w
while x < self.w:
surface.blit(img, (x, 0))
x += w