# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

CHROMA SOLSTICE — a grid-based color-puzzle game written in Lua for the [LÖVE](https://love2d.org) framework. There is no build step, no test suite, and no package manifest; the whole game is the tree of `.lua` files loaded by `main.lua`.

## Running

```sh
love .        # run from the project root
```

`love` is not installed in this environment — you cannot run the game here; reason about the code statically.

**LÖVE version matters.** The code targets the **0.10.x** API, not 11.x. Signals of this that you must preserve when editing:
- Colors are `0–255` integers (`love.graphics.setColor(255, 255, 255)`), not `0–1` floats.
- `sprite:getData()` / `img:getPixel` are used (see `sprite_manager.lua`); the 11.x replacements differ.
Do not "modernize" these to the 11.x API unless explicitly asked — it would break the whole render/color path.

## The core mechanic (read this before touching gameplay code)

The player is not one entity — it is **up to three simultaneous colored copies** (`red`, `green`, `blue`), each living on its own collision layer. This RGB-channel model drives almost every design decision:

- **`Room.activeColors`** — which channels are currently "on". Arrow keys move *every* active-colored copy at once (`Room:movePlayer`); keys `1`/`2`/`3` switch to a single active color (`Room:switchPlayerColor` → `Input.playerColor`).
- **Per-color collision matrices** — `Room.collidableMatrices` / `switchMatrices` are `{red=grid, green=grid, blue=grid}`, each an 11×11 grid of entity references rebuilt every tick. A move is legal only if it's legal on *all* active channels. `Room:movePlayer` does a flood-fill push check *and* a symmetric pull check per color.
- **Merging** — `Room:playerJoinCheck` activates an inactive color when all active copies land on top of it, and deactivates a color that couldn't move. This overlap logic is the puzzle heart; change it carefully.
- **Additive rendering = color mixing.** `love.draw` renders the room to `gameCanvas` with blend mode `"screen"`, so overlapping red+green+blue copies literally add up to white on screen. The visual is downstream of the mechanic.
- **Switches → doors.** `Room:switchCheck` reports, per color, whether *all* switches of that color are pressed; `Door:setColors` unlocks only when all three channels are satisfied.

## Code structure

- `main.lua` — LÖVE callbacks (`love.load/update/draw/keypressed`) and `_init*` globals setup. Also holds the post-process bloom/feedback loop in `love.draw` and the `worldSha` screen-jitter amount (adjust with `w`/`s`).
- `entity.lua` — `Entity` base class (middleclass). Everything on the grid subclasses it: `Player`, `Box`, `Wall`, `Switch`, `Door`, `Artifact`, `Art`, `Ass` (assorted decor), `npc`. Grid↔pixel conversion, occupied-cell shapes, and the `sha()` jitter all live here.
- `room.lua` — the orchestrator: entity registries, per-color matrices, movement/push/pull, tick, switch/door logic, room transitions (`nextRoom`), and serialization.
- `color.lua` — `Color` class + the canonical RGB(W) table. `gColor` globals are created in `main._initGlobalColors`.
- `sprite_manager.lua` — loads all images into the `sprites` global and precomputes `spriteCells` (which 16×16 cells of a sprite are non-empty, for multi-cell collision shapes). Sprites are `16px`-per-cell.
- `input.lua`, `shaders.lua` (`wiggle`, `glow` GLSL), `_helpers.lua` (`lerp`/`map`/`constrain`), `_debug_helpers.lua` (`printMatrix`, `shallowTablePrint`, `randomPosition`).
- `dialogue_manager.lua` is currently empty; `npc.lua`/`art.lua`/`artifact.lua` are stubs or reference art folders not present in `art/`.

### Conventions
- Many globals are intentional (`gridWidth`, `gridHeight`, `drawScale`, `width`, `height`, `sprites`, `gColor`, `currentRoom`, `worldSha`). New shared state generally follows the same pattern rather than modules.
- Comments and some identifiers have a missing/typo'd `k` (`bloc_`, `chec_`, `li_e`) — a quirk of the author's keyboard, not a naming scheme. Match surrounding style; don't mass-fix.

## Levels & serialization

- Rooms live in `rooms/*.sav`, serialized via `libs/TSerial.lua` (`TSerial.pack`/`unpack`). `Room:save` writes to LÖVE's save directory; `Room:load` reads from there.
- **Schema drift warning:** the existing `.sav` files (e.g. `rooms/start.sav`) use a *newer* schema — keys like `static_walls`, `color_changing_floor_tiles`, `players`, `sages`, `exits`, `wall_shake` — that `Room:serialize`/`:deserialize` in `room.lua` does **not** yet read or write (that code keys off class names like `Wall`, `Box`, `Switch`). If a room won't load, this mismatch is the likely cause; reconcile the loader with the on-disk format rather than assuming either side is correct.

## Art pipeline

Sprites are authored in full color, then split into per-channel `_r`/`_g`/`_b` PNGs so each color copy draws its own layer. The `art/*.sh` scripts drive this with ImageMagick `convert`:
- `reduce_colors.sh <file>` — remap to `art/palette`/`palette.png`.
- `extract.sh <file>` — split one image into R/G/B channel PNGs.
- `reduce_and_extract.sh` — both steps over every PNG in the cwd (run from inside an `art/` subfolder).

## Bundled libraries (`libs/`, do not edit)

`middleclass` (OOP), `hump` (only `timer` is used), `sfxrlua` (procedural SFX — the looping static in `_initSounds`), `json4lua`, `TSerial` (level save format).
