# Sprite assets Art: Bolt himself — a cream shepherd pup with a slate cap, a lightning blaze on his forehead and a bolt tag on his collar. The frames are **generated, not hand-drawn**: `scripts/generate_bolt_sprites.py` draws every one of them with Pillow and writes this folder. ```bash python scripts/generate_bolt_sprites.py # rewrite this folder python scripts/generate_bolt_sprites.py --out /tmp/prev # preview elsewhere first python scripts/generate_bolt_sprites.py --states idle # just one state ``` That means tweaking the art is editing code, not 24 PNGs: the palette is a block of constants at the top of the script, the body/head/ear/tail shapes are one function each in normalised 0..1 coordinates, and each state's animation is a list of pose dicts in `frames_for()`. Everything is super-sampled 4x and downscaled on save, because PIL's draw primitives have no antialiasing. **Regenerate after editing** — the PNGs here are committed, so a change to the script alone doesn't move the pet. Convention the loader (`bolt_pet/ui/sprite.py`) expects: ``` assets/sprites/ idle/ frame_00..07.png breathing, tail wag, blink on frame 06 listening/ frame_00..03.png ears perked, head tilted in, collar tag lit, sound arcs thinking/ frame_00..05.png eyes up, head cocked, cycling dots talking/ frame_00..03.png mouth open/close with tongue, ears bouncing error/ frame_00..01.png X eyes, ears drooped, red spark walk/ frame_00..07.png side-view walk cycle (see below) ``` - One subfolder per pet state (matches `bolt_pet.state.PetState`), **plus `walk/`**, which is not a state — see below. - Any `*.png` filenames work — they're played back in alphabetical-sort order, looping, at `IDLE_ANIMATION_FPS` (see `.env`). At the default 6fps the 8-frame idle loop runs about 1.3s. - Frames are square (320px, 2x the default `PET_SIZE` of 160) so they downscale cleanly; the loader scales to fit `PET_SIZE` keeping aspect ratio and centres them in the square pet window. - A state directory with no frames in it falls back to a small procedurally-drawn placeholder blob (see `_placeholder_frames` in `sprite.py`). ## The walk cycle `walk/` is the one animation that isn't a `PetState`. Walking is a property of *movement* — orthogonal to whether he's idle, listening or talking — so it stays out of the state machine and is keyed by name instead (`sprite.EXTRA_ANIMATIONS`). `PetWindow` uses it whenever the pet is actually travelling and falls back to the state animation the moment it stops. Three things about it are load-bearing if you redraw it: - **It's a side view, drawn facing right.** The other poses are a front-facing sit, which is fine standing still but slides like a chess piece when moving. `PetWindow._oriented()` mirrors the frames (cached) when he walks left, so only the right-facing version exists on disk. - **The cycle is advanced by distance travelled, not by the animation timer** (`_WALK_PIXELS_PER_FRAME`, one frame per ~13px). That's what keeps a planted paw tracking backwards at exactly the speed the window moves forwards. Drive it off the clock and the feet skate whenever `PET_WANDER_SPEED` doesn't happen to match `IDLE_ANIMATION_FPS`. If you change the number of frames or the stride length in `paw_position()`, retune that constant to match or he'll moonwalk. - **The frames carry their own vertical bob**, so the window's own bob is switched off while they're in use. Only the no-walk-art fallback still bobs in code. Delete `walk/` and everything still runs — he reverts to sliding with a small coded bob, which is what the pet did before the cycle existed. ## Swapping in different art Replace any state's PNGs (same alphabetical-order-loops convention) to change its look — no code changes needed, and nothing forces you to keep using the generator. If your source is a single grid spritesheet (rows/cols of frames in one PNG) rather than one-file-per-frame, use `scripts/slice_spritesheet.py` to cut it into this folder-of-frames convention: ```bash python scripts/slice_spritesheet.py path/to/idle_sheet.png assets/sprites/idle \ --cols 6 --rows 1 ``` If your format is something else entirely (a single animated GIF/APNG, a Spine/DragonBones skeletal export, an Aseprite `.json` atlas, etc.) — tell me the format and I'll adapt `sprite.py`'s loader rather than making you convert by hand.