Format code with black

This commit is contained in:
James Campbell
2026-08-01 12:19:46 -04:00
parent b0ad9610be
commit f29d8c953c
16 changed files with 459 additions and 311 deletions

View File

@@ -17,10 +17,12 @@ import pygame
from . import settings
# --- helpers -----------------------------------------------------------------
_DIRS = {
"up": (0, -1), "down": (0, 1), "left": (-1, 0), "right": (1, 0),
"up": (0, -1),
"down": (0, 1),
"left": (-1, 0),
"right": (1, 0),
}
@@ -36,6 +38,7 @@ _DIRS = {
# trigger: { timer: { interval: 1.2, up_time: 0.7 } }
# trigger: { all: [ { within: 3 }, { dir: above, aligned: true } ] }
class _Always:
def evaluate(self, trap, game, dt):
return True
@@ -46,6 +49,7 @@ class _Always:
class _Within:
"""Player within N tiles of the trap centre (Euclidean radius)."""
def __init__(self, n):
self.n = float(n)
@@ -69,6 +73,7 @@ class _Directional:
left/right (same rows) or *directly* above/below (same columns).
inclusive: specify if the trap tile itself counts in the given direction.
"""
def __init__(self, direction, rng, aligned, inclusive):
self.dir = direction
self.rng = None if rng is None else float(rng)
@@ -113,6 +118,7 @@ class _Directional:
class _Timer:
"""Cyclic: hidden for ``interval`` seconds, then active for ``up_time``."""
def __init__(self, interval, up_time):
self.interval = float(interval)
self.up_time = float(up_time)
@@ -155,7 +161,9 @@ def make_condition(spec):
if spec == "always":
return _Always()
if not isinstance(spec, dict):
raise ValueError(f"trigger must be 'always' or a condition object, got {spec!r}")
raise ValueError(
f"trigger must be 'always' or a condition object, got {spec!r}"
)
if "all" in spec:
return _All([make_condition(s) for s in spec["all"]])
if "any" in spec:
@@ -166,7 +174,12 @@ def make_condition(spec):
if "within" in spec:
return _Within(spec["within"])
if "dir" in spec:
return _Directional(spec["dir"], spec.get("range"), spec.get("aligned", False), spec.get("inclusive", False))
return _Directional(
spec["dir"],
spec.get("range"),
spec.get("aligned", False),
spec.get("inclusive", False),
)
raise ValueError(f"unrecognized trigger condition: {spec!r}")
@@ -178,7 +191,7 @@ class Trap:
at = spec.get("at", [0, 0])
self.col, self.row = int(at[0]), int(at[1])
self.base_rect = level.cell_rect(self.col, self.row)
self._mounted = False # set True on traps that ride another (a mount)
self._mounted = False # set True on traps that ride another (a mount)
# Any trap can be made invisible (still functional; revealed in debug).
self.invisible = bool(spec.get("invisible", False))
@@ -274,13 +287,14 @@ class Trap:
def _follow(self, parent):
ox, oy = self._mount_off
pr = parent.current_rect()
self.base_rect = pygame.Rect(pr.x + ox, pr.y + oy,
self.base_rect.w, self.base_rect.h)
self.base_rect = pygame.Rect(
pr.x + ox, pr.y + oy, self.base_rect.w, self.base_rect.h
)
def tick(self, dt, game):
self.update(dt, game)
for c in self.mounts:
c._follow(self) # reposition after we've moved this frame
c._follow(self) # reposition after we've moved this frame
c.tick(dt, game)
def render(self, surface, assets):
@@ -354,6 +368,7 @@ class Spike(Trap):
direction: which edge of the cell the spike sits on (up/down/left/right).
See the trigger-condition docs at the top of this module.
"""
def __init__(self, spec, level):
super().__init__(spec, level)
self.direction = spec.get("direction", "up")
@@ -369,13 +384,13 @@ class Spike(Trap):
t = self.tile
r = self.base_rect
dx, dy = _DIRS.get(self.direction, (0, -1))
if dy == -1: # up: bottom half is ground, tip points up
if dy == -1: # up: bottom half is ground, tip points up
return pygame.Rect(r.x, r.y + t // 2, t, t // 2)
if dy == 1: # down (ceiling spike)
if dy == 1: # down (ceiling spike)
return pygame.Rect(r.x, r.y, t, t // 2)
if dx == -1: # left (from right wall pointing left)
if dx == -1: # left (from right wall pointing left)
return pygame.Rect(r.x + t // 2, r.y, t // 2, t)
return pygame.Rect(r.x, r.y, t // 2, t) # right
return pygame.Rect(r.x, r.y, t // 2, t) # right
# CCW rotation to point the (up-facing) sprite the right way.
_ANGLE = {"up": 0, "left": 90, "down": 180, "right": -90}
@@ -415,6 +430,7 @@ class Block(Trap):
start extending and be clear for ``release`` seconds to start
retracting — hysteresis that stops boundary jitter.
"""
def __init__(self, spec, level):
super().__init__(spec, level)
t = self.tile
@@ -429,15 +445,23 @@ class Block(Trap):
self.speed = float(spec.get("speed", 140.0))
self.mode = spec.get("mode", "once")
self.deadly = bool(spec.get("deadly", False))
self.fake = bool(spec.get("fake", False)) # looks solid, isn't
self.fake = bool(spec.get("fake", False)) # looks solid, isn't
self.crumble = bool(spec.get("crumble", False)) # gives way when stood on
self.crumble_delay = float(spec.get("crumble_delay", 0.4))
self.respawn = float(spec.get("respawn", 2.5))
self.sprite = spec.get("sprite",
"spike_block" if self.deadly else
"fake_block" if self.fake else
"crumble_block" if self.crumble else "moving_block")
self.release = float(spec.get("release", 0.1)) # once-mode retract hysteresis
self.sprite = spec.get(
"sprite",
(
"spike_block"
if self.deadly
else (
"fake_block"
if self.fake
else "crumble_block" if self.crumble else "moving_block"
)
),
)
self.release = float(spec.get("release", 0.1)) # once-mode retract hysteresis
# `home` (default): sense the trigger from the resting cell, so the block
# moving away can't toggle its own trigger (no jitter). `current`: sense
# from the live position — for blocks the player rides (e.g. a dropper),
@@ -463,8 +487,9 @@ class Block(Trap):
# `home` sensing tracks the resting cell as it rides along the parent.
ox, oy = self._origin
offx, offy = self._mount_off
self.base_rect = pygame.Rect(round(ox + offx), round(oy + offy),
self.tile, self.tile)
self.base_rect = pygame.Rect(
round(ox + offx), round(oy + offy), self.tile, self.tile
)
def reset(self):
self._reset_trigger()
@@ -474,12 +499,12 @@ class Block(Trap):
# LOCAL — the live rect is always _origin + (x, y).
self._origin = (0.0, 0.0)
self.prev = (self.x, self.y)
self.dir = 1 # pingpong direction
self.phase = "rest" # once mode: rest|extending|extended|retracting
self.dir = 1 # pingpong direction
self.phase = "rest" # once mode: rest|extending|extended|retracting
self._release_t = 0.0
# index of the waypoint we're AT (once) / heading toward (patrol)
self.idx = 0 if self.mode == "once" else (1 if len(self.points) > 1 else 0)
self.cstate = "solid" # crumble: solid|crumbling|gone
self.cstate = "solid" # crumble: solid|crumbling|gone
self.ctimer = 0.0
self.shake = 0.0
self.emerge_kill = False
@@ -491,7 +516,7 @@ class Block(Trap):
# block executes its path/move relative to the parent it rides.
if not self._mounted:
self.prev = (self.x, self.y)
active = self.triggered(game, dt) # call every frame to keep the timer live
active = self.triggered(game, dt) # call every frame to keep the timer live
if len(self.points) >= 2:
step = self.speed * dt
if self.mode == "once":
@@ -505,8 +530,9 @@ class Block(Trap):
self.emerge_kill = False
r = self._rect()
p = game.player.rect
on_top = (abs(p.bottom - r.top) <= 4
and p.right > r.left + 2 and p.left < r.right - 2)
on_top = (
abs(p.bottom - r.top) <= 4 and p.right > r.left + 2 and p.left < r.right - 2
)
if self.cstate == "solid":
if on_top:
self.cstate = "crumbling"
@@ -590,8 +616,7 @@ class Block(Trap):
def _rect(self):
ox, oy = self._origin
return pygame.Rect(round(ox + self.x), round(oy + self.y),
self.tile, self.tile)
return pygame.Rect(round(ox + self.x), round(oy + self.y), self.tile, self.tile)
def current_rect(self):
return self._rect()
@@ -654,6 +679,7 @@ class ArrowShooter(Trap):
direction: up/down/left/right. speed: px/s. interval: seconds between shots.
trigger: only fires while the condition holds (default ``always``).
"""
def __init__(self, spec, level):
super().__init__(spec, level)
self.direction = spec.get("direction", "left")
@@ -676,10 +702,14 @@ class ArrowShooter(Trap):
rect = pygame.Rect(0, 0, w, h)
rect.center = r.center
# nudge the arrow to the emitting edge
if dx == -1: rect.right = r.left
elif dx == 1: rect.left = r.right
elif dy == -1: rect.bottom = r.top
elif dy == 1: rect.top = r.bottom
if dx == -1:
rect.right = r.left
elif dx == 1:
rect.left = r.right
elif dy == -1:
rect.bottom = r.top
elif dy == 1:
rect.top = r.bottom
self.arrows.append(Arrow(rect, dx * self.speed, dy * self.speed))
def update(self, dt, game):
@@ -704,8 +734,9 @@ class ArrowShooter(Trap):
return [a.rect for a in self.arrows]
def draw(self, surface, assets):
surface.blit(assets.get("arrow_shooter", self.tile, self.tile),
self._rt(self.base_rect))
surface.blit(
assets.get("arrow_shooter", self.tile, self.tile), self._rt(self.base_rect)
)
for a in self.arrows:
surface.blit(assets.get("arrow", a.rect.w, a.rect.h), self._rt(a.rect))
@@ -717,7 +748,8 @@ class Warp(Trap):
destination and fades away, so the (otherwise invisible) teleport reads on
screen. The level's ``debug`` flag tints it (and draws a line to its
destination) while designing."""
_PULSE_TIME = 0.35 # seconds the activation aura takes to fade out
_PULSE_TIME = 0.35 # seconds the activation aura takes to fade out
# Concentric rings of the aura: (radius x tile, alpha x, colour). Drawn
# largest-first so the bright core sits on top.
@@ -735,8 +767,8 @@ class Warp(Trap):
self.reset()
def reset(self):
self._armed = True # re-arms once the player has left the tile
self._pulse = 0.0 # 1.0 at activation, fades to 0 over _PULSE_TIME
self._armed = True # re-arms once the player has left the tile
self._pulse = 0.0 # 1.0 at activation, fades to 0 over _PULSE_TIME
def update(self, dt, game):
if self._pulse > 0.0:
@@ -749,7 +781,7 @@ class Warp(Trap):
p.vx = p.vy = 0.0
p._sync_rect()
self._armed = False
self._pulse = 1.0 # flash at both ends this frame, then fade
self._pulse = 1.0 # flash at both ends this frame, then fade
elif not inside:
self._armed = True
@@ -763,8 +795,9 @@ class Warp(Trap):
c.render(surface, assets)
def _dest_rect(self):
return pygame.Rect(self.dest[0] * self.tile, self.dest[1] * self.tile,
self.tile, self.tile)
return pygame.Rect(
self.dest[0] * self.tile, self.dest[1] * self.tile, self.tile, self.tile
)
def _draw_aura(self, surface, rect):
# An expanding, fading glow centred on the cell. As the pulse decays the
@@ -788,8 +821,13 @@ class Warp(Trap):
if self.level.debug:
self._debug_tint(surface, (210, 80, 235), alpha=90)
dest = self._dest_rect()
pygame.draw.line(surface, (210, 80, 235),
self._rt(self.base_rect).center, self._rt(dest).center, 1)
pygame.draw.line(
surface,
(210, 80, 235),
self._rt(self.base_rect).center,
self._rt(dest).center,
1,
)
self._debug_tint(surface, (210, 80, 235), dest, 45)
@@ -799,6 +837,7 @@ class PhaseBlock(Trap):
a solid obstacle over ``fade`` seconds (and fades back out when the trigger
releases). If the player is standing in the cell the instant it *starts*
appearing, they're killed."""
def __init__(self, spec, level):
super().__init__(spec, level)
self.fade = float(spec.get("fade", 0.3))
@@ -814,14 +853,17 @@ class PhaseBlock(Trap):
# How deep the player may be into the cell and still be nudged clear rather
# than killed. A shallow clip (feet/shoulder in the cell) gets shoved out;
# forming through their middle stays lethal.
_EDGE_GRACE = 0.5 # fraction of a tile
_EDGE_GRACE = 0.5 # fraction of a tile
def update(self, dt, game):
self.emerge_kill = False
active = self.triggered(game, dt)
if active:
if self.alpha == 0.0 and not self.solid \
and game.player.rect.colliderect(self.base_rect):
if (
self.alpha == 0.0
and not self.solid
and game.player.rect.colliderect(self.base_rect)
):
# Forming into the player. If they're only clipping an edge, shove
# them clear and let the block solidify behind them; only if it's
# forming through their middle — or the shove would squish them
@@ -847,15 +889,15 @@ class PhaseBlock(Trap):
b = self.base_rect
# Distance to move the player to clear the block on each side.
outs = {
"up": p.bottom - b.top,
"down": b.bottom - p.top,
"left": p.right - b.left,
"up": p.bottom - b.top,
"down": b.bottom - p.top,
"left": p.right - b.left,
"right": b.right - p.left,
}
side = min(outs, key=outs.get)
dist = outs[side]
if dist > self.tile * self._EDGE_GRACE:
return False # deep overlap — forming through them
return False # deep overlap — forming through them
dx, dy = _DIRS[side]
moved = p.move(dx * dist, dy * dist)
# The block isn't solid yet, so it's absent from solid_rects(); any hit
@@ -891,8 +933,9 @@ class PhaseBlock(Trap):
self._debug_tint(surface, (120, 210, 240), alpha=45)
return
img = assets.get("phase_block", self.tile, self.tile).copy()
img.fill((255, 255, 255, int(255 * self.alpha)),
special_flags=pygame.BLEND_RGBA_MULT)
img.fill(
(255, 255, 255, int(255 * self.alpha)), special_flags=pygame.BLEND_RGBA_MULT
)
surface.blit(img, self._rt(self.base_rect))