267 lines
9.3 KiB
Python
267 lines
9.3 KiB
Python
"""Zeichenroutinen – alles, was Pixel auf den Bildschirm bringt."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pygame
|
||
|
||
from . import config
|
||
from .case import Case
|
||
from .sequences import CaseSpec
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Hintergrund-Bänder
|
||
# --------------------------------------------------------------------------
|
||
def draw_top_bar(surface: pygame.Surface, assets, percent: float, subtitle: str = "") -> None:
|
||
surface.fill(
|
||
config.COLOR_TOP_BAR, (0, 0, config.SCREEN_WIDTH, config.TOP_BAR_HEIGHT)
|
||
)
|
||
font = assets.font
|
||
title = font.render(config.TITLE_TEXT, True, config.COLOR_WHITE)
|
||
surface.blit(title, (40, config.TOP_BAR_HEIGHT // 2 - title.get_height() // 2))
|
||
if subtitle:
|
||
small = (assets.font_small or font).render(subtitle, True, config.COLOR_WHITE)
|
||
surface.blit(
|
||
small,
|
||
(
|
||
40 + title.get_width() + 48,
|
||
config.TOP_BAR_HEIGHT // 2 - small.get_height() // 2,
|
||
),
|
||
)
|
||
_draw_progress_bar(surface, assets, percent)
|
||
|
||
|
||
def _draw_progress_bar(surface: pygame.Surface, assets, percent: float) -> None:
|
||
bar_width = config.PROGRESS_BAR_WIDTH
|
||
bar_height = config.PROGRESS_BAR_HEIGHT
|
||
x = config.SCREEN_WIDTH - bar_width - config.PROGRESS_BAR_RIGHT_MARGIN
|
||
y = config.TOP_BAR_HEIGHT // 2 - bar_height // 2
|
||
gap = config.PROGRESS_BAR_GAP
|
||
segment_width = (bar_width - (config.PROGRESS_BAR_SEGMENTS - 1) * gap) / config.PROGRESS_BAR_SEGMENTS
|
||
|
||
red = min(255, int(percent * 2.55))
|
||
green = max(0, 255 - int(percent * 2.55))
|
||
color = (red, green, 0)
|
||
|
||
for index in range(config.PROGRESS_BAR_SEGMENTS):
|
||
segment_x = x + index * (segment_width + gap)
|
||
rect = (segment_x, y, segment_width, bar_height)
|
||
pygame.draw.rect(surface, config.COLOR_PROGRESS_TRACK, rect, border_radius=config.PROGRESS_BAR_RADIUS)
|
||
if percent >= (index + 1) * 20:
|
||
pygame.draw.rect(surface, color, rect, border_radius=config.PROGRESS_BAR_RADIUS)
|
||
|
||
text = assets.font.render(f"{int(percent)}%", True, config.COLOR_WHITE)
|
||
surface.blit(text, (x + bar_width + 10, y))
|
||
|
||
|
||
def draw_background(surface: pygame.Surface, assets) -> None:
|
||
if assets.background is not None:
|
||
surface.blit(assets.background, (0, config.TOP_BAR_HEIGHT))
|
||
else:
|
||
pygame.draw.rect(
|
||
surface,
|
||
config.COLOR_BACKGROUND,
|
||
(0, config.TOP_BAR_HEIGHT, config.SCREEN_WIDTH, config.GAME_AREA_HEIGHT),
|
||
)
|
||
|
||
|
||
def clear_bottom_bar(surface: pygame.Surface) -> None:
|
||
"""Untere Leiste schwärzen (ohne Inhalt)."""
|
||
surface.fill(
|
||
config.COLOR_BOTTOM_BAR,
|
||
(
|
||
0,
|
||
config.TOP_BAR_HEIGHT + config.GAME_AREA_HEIGHT,
|
||
config.SCREEN_WIDTH,
|
||
config.BOTTOM_BAR_HEIGHT,
|
||
),
|
||
)
|
||
|
||
|
||
def draw_bottom_bar(surface: pygame.Surface, assets, upcoming: list[CaseSpec]) -> None:
|
||
clear_bottom_bar(surface)
|
||
top = config.SCREEN_HEIGHT - config.BOTTOM_BAR_HEIGHT
|
||
label = (assets.font_small or assets.font).render(config.NEXT_LABEL, True, config.COLOR_WHITE)
|
||
surface.blit(label, (40, top + 12))
|
||
|
||
preview_x = 40
|
||
for index, spec in enumerate(upcoming[: config.NEXT_PREVIEW_COUNT]):
|
||
image = _preview_surface(assets, spec)
|
||
height = image.get_height() if image is not None else 0
|
||
y = config.SCREEN_HEIGHT - config.NEXT_PREVIEW_MARGIN - height
|
||
if image is None:
|
||
width, height = _spec_size(spec)
|
||
y = config.SCREEN_HEIGHT - config.NEXT_PREVIEW_MARGIN - height
|
||
pygame.draw.rect(
|
||
surface,
|
||
config.CASE_COLORS[index % len(config.CASE_COLORS)],
|
||
(preview_x, y, width, height),
|
||
)
|
||
else:
|
||
width = image.get_width()
|
||
surface.blit(image, (preview_x, y))
|
||
preview_x += max(width, 60) + 20
|
||
|
||
|
||
_PREVIEW_CACHE: dict[tuple[int, int], pygame.Surface | None] = {}
|
||
|
||
|
||
def _preview_surface(assets, spec: CaseSpec) -> pygame.Surface | None:
|
||
"""Vorschau-Grafik in verkleinertem Massstab (gecacht)."""
|
||
key = (spec.units_w, spec.units_h)
|
||
if key in _PREVIEW_CACHE:
|
||
return _PREVIEW_CACHE[key]
|
||
image = assets.case_surface(spec.units_w, spec.units_h) if assets is not None else None
|
||
if image is not None:
|
||
size = (
|
||
max(1, int(image.get_width() * config.NEXT_PREVIEW_SCALE)),
|
||
max(1, int(image.get_height() * config.NEXT_PREVIEW_SCALE)),
|
||
)
|
||
image = pygame.transform.smoothscale(image, size)
|
||
_PREVIEW_CACHE[key] = image
|
||
return image
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Auflieger
|
||
# --------------------------------------------------------------------------
|
||
def draw_trailer(surface: pygame.Surface, offset: tuple[int, int] = (0, 0)) -> None:
|
||
pygame.draw.rect(
|
||
surface,
|
||
config.COLOR_TRAILER,
|
||
(
|
||
config.TRAILER_X + offset[0],
|
||
config.TRAILER_Y + offset[1],
|
||
config.TRAILER_WIDTH,
|
||
config.TRAILER_HEIGHT,
|
||
),
|
||
)
|
||
|
||
|
||
def draw_wall(surface: pygame.Surface, wall_x: float, offset: tuple[int, int] = (0, 0)) -> None:
|
||
pygame.draw.line(
|
||
surface,
|
||
config.COLOR_WALL,
|
||
(wall_x + offset[0], config.TRAILER_Y + offset[1]),
|
||
(wall_x + offset[0], config.TRAILER_BOTTOM + offset[1]),
|
||
3,
|
||
)
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Koffer
|
||
# --------------------------------------------------------------------------
|
||
def _spec_size(spec: CaseSpec) -> tuple[int, int]:
|
||
return (
|
||
int(spec.units_w * config.UNIT_LENGTH),
|
||
int(spec.units_h * config.UNIT_HEIGHT),
|
||
)
|
||
|
||
|
||
def draw_case(surface: pygame.Surface, assets, case: Case, angle: float = 0.0) -> None:
|
||
"""Zeichnet einen Koffer – immer mit **seiner eigenen** Textur.
|
||
|
||
Ein gedrehter Koffer wird um 90° gedreht gezeichnet, bekommt aber keine
|
||
andere Grafik.
|
||
"""
|
||
x, y = int(case.x), int(case.y)
|
||
width, height = int(case.width), int(case.height)
|
||
image = assets.case_surface(*case.texture) if assets is not None else None
|
||
if image is None:
|
||
pygame.draw.rect(surface, case.color, (x, y, width, height))
|
||
return
|
||
if case.is_rotated:
|
||
image = pygame.transform.rotate(image, -90)
|
||
if angle:
|
||
rotated = pygame.transform.rotate(image, angle)
|
||
surface.blit(rotated, rotated.get_rect(bottomleft=(x, y + height)))
|
||
else:
|
||
surface.blit(image, (x, y))
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Hinweise und Overlays
|
||
# --------------------------------------------------------------------------
|
||
def draw_hint(surface: pygame.Surface, assets, stack_possible: bool) -> None:
|
||
if stack_possible:
|
||
text, color = config.HINT_STACK, config.COLOR_HINT_STACK
|
||
else:
|
||
text, color = config.HINT_SNAP, config.COLOR_HINT_SNAP
|
||
rendered = assets.font.render(text, True, color)
|
||
surface.blit(
|
||
rendered,
|
||
(
|
||
config.SCREEN_WIDTH - config.SCREEN_WIDTH // 3 - rendered.get_width() // 2,
|
||
config.SCREEN_HEIGHT - config.BOTTOM_BAR_HEIGHT // 2 - rendered.get_height() // 2,
|
||
),
|
||
)
|
||
|
||
|
||
def draw_dim(surface: pygame.Surface, alpha: int = 170) -> None:
|
||
"""Dunkelt das ganze Bild ab (für Menü und Ergebnis)."""
|
||
overlay = pygame.Surface((config.SCREEN_WIDTH, config.SCREEN_HEIGHT))
|
||
overlay.set_alpha(alpha)
|
||
overlay.fill((0, 0, 0))
|
||
surface.blit(overlay, (0, 0))
|
||
|
||
|
||
def _wrap_text(text: str, font: pygame.font.Font, max_width: int) -> list[str]:
|
||
"""Bricht eine Zeile an Wortgrenzen um, wenn sie zu breit würde."""
|
||
words = text.split()
|
||
if not words:
|
||
return [text]
|
||
lines: list[str] = []
|
||
current = words[0]
|
||
for word in words[1:]:
|
||
candidate = f"{current} {word}"
|
||
if font.size(candidate)[0] <= max_width:
|
||
current = candidate
|
||
else:
|
||
lines.append(current)
|
||
current = word
|
||
lines.append(current)
|
||
return lines
|
||
|
||
|
||
def draw_overlay_lines(
|
||
surface: pygame.Surface,
|
||
assets,
|
||
lines: list[str],
|
||
) -> None:
|
||
"""Dunkler Schleier mit zentrierten Textzeilen (Ergebnis-Anzeige).
|
||
|
||
Die erste Zeile wird gross gesetzt, alle weiteren klein – so passen auch
|
||
längere Sprüche auf den Schirm.
|
||
"""
|
||
draw_dim(surface, 220)
|
||
|
||
big = assets.font
|
||
small = assets.font_small or big
|
||
max_width = config.SCREEN_WIDTH - 200
|
||
|
||
rendered: list[pygame.Surface] = []
|
||
for index, line in enumerate(lines):
|
||
font = big if index == 0 else small
|
||
for part in _wrap_text(line, font, max_width):
|
||
rendered.append(font.render(part, True, config.COLOR_WHITE))
|
||
|
||
line_gap = 18
|
||
total_height = sum(image.get_height() + line_gap for image in rendered)
|
||
y = config.SCREEN_HEIGHT // 2 - total_height // 2
|
||
|
||
# Dunkles Band hinter dem Text, damit er auch über Koffern lesbar bleibt.
|
||
widest = max(image.get_width() for image in rendered)
|
||
band = pygame.Surface((widest + 120, total_height + 20), pygame.SRCALPHA)
|
||
band.fill((0, 0, 0, 170))
|
||
surface.blit(
|
||
band,
|
||
(
|
||
config.SCREEN_WIDTH // 2 - band.get_width() // 2,
|
||
y - 10,
|
||
),
|
||
)
|
||
|
||
for image in rendered:
|
||
surface.blit(image, (config.SCREEN_WIDTH // 2 - image.get_width() // 2, y))
|
||
y += image.get_height() + line_gap
|