ajhahn.de
← the-way-out
Python 114 lines
import pygame

from settings import TILE_SIZE

TS = TILE_SIZE


def _shade(color, d):
    """Lighten (d > 0) or darken (d < 0) a color, clamped to 0-255."""
    return tuple(max(0, min(255, c + d)) for c in color)


class TileTextures:
    """Lazily-built, cached 64x64 tile surfaces.

    One surface per kind is reused for every tile of that kind, so a
    4608x2944 level costs a handful of Surface allocations, not thousands.
    Built on first use (after the display exists).
    """

    WALL_BASE = (54, 52, 70)
    FLOOR_BASE = (32, 31, 44)

    _cache = {}

    @classmethod
    def _build_wall(cls):
        s = pygame.Surface((TS, TS)).convert()
        s.fill(cls.WALL_BASE)

        # Two-brick course with mortar lines for a dungeon-stone read.
        mortar = _shade(cls.WALL_BASE, -22)
        pygame.draw.line(s, mortar, (0, TS // 2), (TS, TS // 2), 3)
        pygame.draw.line(s, mortar, (TS // 2, 0), (TS // 2, TS // 2), 3)
        pygame.draw.line(s, mortar, (TS // 4, TS // 2), (TS // 4, TS), 3)
        pygame.draw.line(s, mortar, (3 * TS // 4, TS // 2), (3 * TS // 4, TS), 3)

        # Bevel: lit top/left, shadowed bottom/right -> blocks pop.
        hi = _shade(cls.WALL_BASE, 30)
        lo = _shade(cls.WALL_BASE, -34)
        pygame.draw.line(s, hi, (0, 0), (TS - 1, 0), 3)
        pygame.draw.line(s, hi, (0, 0), (0, TS - 1), 3)
        pygame.draw.line(s, lo, (0, TS - 1), (TS - 1, TS - 1), 3)
        pygame.draw.line(s, lo, (TS - 1, 0), (TS - 1, TS - 1), 3)
        return s

    @classmethod
    def _build_floor(cls, alt):
        base = cls.FLOOR_BASE if not alt else _shade(cls.FLOOR_BASE, 5)
        s = pygame.Surface((TS, TS)).convert()
        s.fill(base)
        pygame.draw.rect(s, _shade(base, -10), (0, 0, TS, TS), 1)
        pygame.draw.rect(s, _shade(base, 8), (4, 4, TS - 8, TS - 8), 1)
        return s

    @classmethod
    def get(cls, kind):
        if kind not in cls._cache:
            if kind == 'wall':
                cls._cache[kind] = cls._build_wall()
            elif kind == 'floor':
                cls._cache[kind] = cls._build_floor(False)
            elif kind == 'floor_alt':
                cls._cache[kind] = cls._build_floor(True)
            else:  # unknown -> magenta marker, easy to spot
                surf = pygame.Surface((TS, TS)).convert()
                surf.fill((120, 40, 120))
                cls._cache[kind] = surf
        return cls._cache[kind]


class StaticObject(pygame.sprite.Sprite):
    pass


class Tile(pygame.sprite.Sprite):
    """A static map cell.

    Walls go into the obstacle group for collision; the level pre-renders
    the look into one big surface, so every wall shares one cached image
    (no per-tile Surface allocation).
    """

    def __init__(self, pos, groups, sprite_type, surface=None):
        super().__init__(groups)
        self.sprite_type = sprite_type
        self.image = surface if surface is not None else TileTextures.get(
            sprite_type if sprite_type in ('wall', 'floor') else 'wall')
        self.rect = self.image.get_rect(topleft=pos)
        # Trim the vertical hitbox slightly so corners feel less sticky.
        self.hitbox = self.rect.inflate(0, -8)


class Prop(pygame.sprite.Sprite):
    """A tileset furniture/decoration object placed from the map.

    The art is bottom-anchored inside its tile (see ``tileset._fit``).
    ``solid`` props join the obstacle group so the player can't walk
    through them — their hitbox is just the lower footprint so you can
    still slip past the visual top. Decorations are draw-only.
    """

    def __init__(self, pos, image, solid=False, obstacle_group=None):
        super().__init__()
        self.image = image
        self.rect = self.image.get_rect(topleft=pos)
        if solid:
            self.hitbox = self.rect.inflate(-10, -TILE_SIZE // 2)
            self.hitbox.bottom = self.rect.bottom - 4
            if obstacle_group is not None:
                obstacle_group.add(self)
        else:
            self.hitbox = self.rect.copy()