# 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 or package manifest; the whole game is the tree of `.lua` files loaded by `main.lua`. ## Running ```sh love . # run from the project root lua tests/test_movement.lua # run the pure matrix-movement tests ``` `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 **11.x** API — `conf.lua` pins `t.version = "11.5"`. Preserve these when editing: - Colors are normalized `0–1` floats (`love.graphics.setColor(1, 1, 1)`), not `0–255` integers — see `color.lua`. - Pixel inspection uses the 11.x ImageData API: `love.image.newImageData(path)` + `ImageData:getPixel` (see `getCellsFromSprite` in `_helpers.lua`). `Image:getData()` was removed in 11 — don't reintroduce it. Do not "downgrade" these to the 0.10.x API — 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 loading: `loadSprite(path)` in `_helpers.lua` returns a cached `Image`. Per-asset occupied-cell shapes (which 16×16 cells are non-empty, for multi-cell collision) are precomputed by `getCellsFromSprite` (`_helpers.lua`) and gathered into `globalAssetProperties` by `getAllAssets` (`level_editor/object_attributes.lua`). Sprites are `16px`-per-cell. (An older `sprite_manager.lua` that populated `sprites`/`spriteCells` globals is gone; the stray `sprites.*` / `spriteCells[...]` references in stub files like `artifact.lua` / `assorted.lua` are vestigial.) - `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 ` — remap to `art/palette`/`palette.png`. - `extract.sh ` — 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).