chroma_solstice/CLAUDE.md
Your Name c348b81a93 Add named Marks and cross-room NPC pathfinding
- Mark: a debug non-collidable named destination (pixel-art target icon),
  editor tool + name modal, globally-unique names, programmatic Room:addMark
- NPCs walk to Marks: each RGB copy is a per-room resident (like the player),
  running its own A* on its own colour layer, transferring rooms as it crosses
  seams; rendered and collided by the room it currently stands in
- Player copies are obstacles in NPC pathfinding
- A* is footprint-aware for multi-cell NPCs
- Editor: click an NPC to open a filterable destination picker
- Ink: Dialogue.open binds walk_to; Jorge can be sent to "end"/"begin"
- Fix: a blocked NPC now replans each tick and resumes instead of freezing
- [npc] move diagnostics under DEBUG (start/goal/route or block reason)
- Docs (adding-npcs) + CLAUDE.md LÖVE 11.x/float-colour corrections
- Fix test harness loadSprite stub; movement tests green (28)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 15:56:53 -04:00

5.9 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

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