chroma_solstice/CLAUDE.md

61 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
**One full-color master PNG per sprite.** The RGB channels are split at *load time*, not stored as separate files: `getAllAssets` (`level_editor/object_attributes.lua`) points all three of `sprites.{red,green,blue}` at the same master, and `Entity:draw(channel)` tints it through `gColor[channel]` — so `master × (1,0,0)` renders the red gun, and the additive `"screen"` blend recombines overlapping copies into the original color. Per-channel collision shapes come from `getChannelCells` (`_helpers.lua`), which inspects each channel of the master's ImageData. Adding a sprite (or a new sprite *state*) is now one file, not three.
- `getAllAssets` treats every master PNG in a non-`_`-prefixed content folder as a placeable color asset, except the names in its `NON_ASSET_SPRITES` set (the palette swatch and sprites owned by dedicated classes like `player_*`, `door_unlocked`, `switch_pressed`).
- The `_r`/`_g`/`_b` channel-splitting step is gone (older revisions had an `extract.sh`; the pre-split channel PNGs live in git history if ever needed).
- `reduce_colors.sh <file>` / `reduce_and_extract.sh` — optional palette remap to `art/palette.png` (authoring step only; produces a master, no split).
## 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).