chroma_solstice/CLAUDE.md

6.7 KiB
Raw Blame History

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 framework. There is no build step or package manifest; the whole game is the tree of .lua files loaded by main.lua.

Running

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).