Scrypt / scrypt /ui /fx.py
IMJONEZZ's picture
SCRYPT: initial commit β€” game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
2.52 kB
"""Terminal effects: the small kit every screen's animation draws from.
Honest about the medium β€” everything here is characters and timing.
SCRYPT_REDUCED_MOTION=1 turns all of it off (accessibility, and the test
suite runs with it set so timing never leaks into assertions).
"""
from __future__ import annotations
import os
import random
from dataclasses import dataclass, field
NOISE_CHARS = "β–’β–‘β–“β–šβ–ž "
def reduced_motion() -> bool:
return os.environ.get("SCRYPT_REDUCED_MOTION", "") not in ("", "0")
def noise_line(width: int, rng: random.Random | None = None) -> str:
rng = rng or random
return "".join(rng.choice(NOISE_CHARS) for _ in range(width))
def corrupt(text: str, fraction: float = 0.12, rng: random.Random | None = None) -> str:
"""A glitched copy: a few characters replaced with static."""
rng = rng or random
chars = list(text)
for i, ch in enumerate(chars):
if ch.strip() and rng.random() < fraction:
chars[i] = rng.choice("β–“β–šβ–žβ––β–—")
return "".join(chars)
@dataclass
class BoardFX:
"""One frame of combat theater, consumed by render.board().
flash (side, lane) cells whose border burns bright this frame
(side: "foe" | "player" | "queue")
dissolve (side, lane) -> noise intensity 0..2 for dying cards
floats lane -> (text, style) shown in the float row between rows
scale overrides the displayed scale (progressive tipping)
"""
flash: set[tuple[str, int]] = field(default_factory=set)
dissolve: dict[tuple[str, int], int] = field(default_factory=dict)
floats: dict[int, tuple[str, str]] = field(default_factory=dict)
scale: int | None = None
@property
def empty(self) -> bool:
return not (self.flash or self.dissolve or self.floats) and self.scale is None
# Typewriter pacing: ticks to hold after revealing certain characters.
PUNCT_PAUSE = {".": 6, "β€”": 5, "…": 6, "?": 5, "!": 5, ",": 3, ";": 3, ":": 3}
# The eye. It watches. Pupil position follows what the player has selected.
EYE_FRAMES = ["(● )", "( ● )", "( ● )", "( ● )", "( ●)"]
EYE_BLINK = "( ──── )"
EYE_WIDE = "((=●=))"
def eye(selected: int, total: int, *, blink: bool = False, wide: bool = False) -> str:
if wide:
return EYE_WIDE
if blink:
return EYE_BLINK
if total <= 1:
return EYE_FRAMES[2]
pos = round(selected / max(1, total - 1) * (len(EYE_FRAMES) - 1))
return EYE_FRAMES[pos]