chroma_solstice/CLAUDE.md
Your Name b19f58726c Add CLAUDE.md project guidance
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 23:15:45 -04:00

5.4 KiB
Raw Permalink 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, no test suite, and no package manifest; the whole game is the tree of .lua files loaded by main.lua.

Running

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 0255 integers (love.graphics.setColor(255, 255, 255)), not 01 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:switchPlayerColorInput.playerColor).
  • Per-color collision matricesRoom.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.
  • MergingRoom: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.luaEntity 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.luaColor 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).