Working in the App¶
Signed in, the App opens on its workshop: one window, where a game is drawn, coded, played and published without ever reloading a page. The words it uses, project, model, map, level, attachment, group, release, are defined in Projects, maps, levels, releases.
The path of a game: upload its sprites and make models of them; draw a map; make a level on it, place the models there and compile the level into the project; write main.lua, ▶ Play, correct, play again; publish.
The window¶
| Where | Panels |
|---|---|
| left | Project (the tree of the open project), Library (everything your account owns) |
| centre | the tabs: maps, levels, files, sprites, sounds, the game being played, the public library |
| right | Inspector: the sheet of whatever is selected, with its fields and buttons |
| bottom | Output, Budget, Tiles, Decors |
The top bar carries the project selector and its + (new project), ▶ Play, Public library, Reset layout, and the links to the simulator, your account and, for administrators, the administration.
A single click selects: the inspector shows the sheet, and if the thing is already open its tab comes to the front. A double-click opens a tab in the centre: a map, a level or a file in the tree, a sprite or a sound in the library. Closing a modified tab asks first: Save, Discard or Cancel. Nothing is saved on its own; leaving the page with changes pending asks too.
Two shortcuts hold everywhere, the code editor included: Ctrl+S (⌘+S) saves the last tab looked at in the centre, Ctrl+Enter (⌘+Enter) plays the project.
Panels are dragged, docked elsewhere and resized; the arrangement is remembered by the browser, and Reset layout puts it back. The sections of the library fold and unfold, remembered too. The address follows the open project and the selected node: it can be bookmarked.
Projects¶
+ in the top bar creates a project from a title (the slug is drawn from it: lowercase, digits, - and _). It is born with a main.lua, _update() and _draw(), the two functions the console calls every frame.
The Project panel only shows the open project:
- Files:
main.luafirst, then the other.luaand.mask. The folder's sheet creates a file (without an extension it is a.lua) and uploads files:.lua,.mask,.son,.sfx, or.wav, converted into an effect; a name you already have is replaced. A file's sheet deletes it, nevermain.lua. The<level>.luaand<level>.maskthat compiling writes are marked generated: they open read-only, and the next compilation rewrites them. - Levels: the levels attached to the project, each with its map (Map: …), its objects and its Compile sheet; a level whose map has changed since its compilation carries a recompile badge. A level attaches itself by compiling, project ticked; there is nothing to "link". A level that is not attached says so on its sheet, with Compile into <project>…; an attached one offers Detach from this project, which also removes the
<level>.luaand<level>.maskits compilation wrote (a.luaof that name written by hand stays). - Maps: the maps of these levels, each once, with their tiles, layers, collision, terrains, decors and sources.
- Audio: the project's sounds, and on the folder's sheet the audio bundles attached to it.
- Versions: what has been published, and the publishing itself.
The project's sheet gives its size and, under What the console allows, the count against the console's bounds: palettes out of 12, backgrounds out of 16, kilobytes out of 2 048. Compiling a level refuses to attach it to a project it would overflow. Export .zip downloads every file of the project; Delete this project asks for its slug to be typed: its published versions stay online.
Playing¶
▶ Play opens the game in a tab of the centre and runs it with the files as they are in the editor, saved or not: you play what you see. Levels play as they were last compiled. ▶ Restart and ■ Stop sit above the screen, with the sound switch and ↻ Auto-restart (both remembered). The game has the keyboard while it has the focus; elsewhere, a veil says Click to play, and Files changed — click to restart when a tab has changed since the launch.
Auto-restart, on by default, restarts the game by itself when a saved file changes or a level is written into the project (saved or compiled): save, and the game plays the new code. Typing in a tab restarts nothing; the veil stays. Turn it off to keep a game running while you edit; turning it back on applies what is pending.
Live changes. While the game runs, a level open in a tab pushes two things into it at once, without a restart: the behaviours you save (a model's, an object's own copy: the objects concerned start again from their first state) and the position of an object you move. Try a speed, watch, try another. Anything else about the objects (one added or removed, another model, visibility, a property) waits for the next save, which restarts the game.
Keys: the arrows are the D-pad, Space or Z is A, X is B; gamepads work too. The line under the screen is the simulator's status bar: the busiest scanline, the display list, the pool, the missing sprites, the Lua heap, the late lines.
The first error freezes the game and goes to Output with a file:line button that opens the file at that line, including a module loaded by require.
The Game sheet of the project (its Game button) writes main.lua from a short configuration — the levels in order, what the pot starts with, the rules that go from one level to the next, the HUD — and the Pot panel, next to Output, shows while the game runs what the behaviours share; see Behaviours. A behaviour saved while the game runs enters it without a restart.
Publishing a version¶
The Versions sheet publishes the game: a few notes, the licence, the box to tick, and Publish vN. A version is frozen: it never changes as you keep working, and everyone can play and clone it. The sheet says whether the project has changed since the last version; a file not saved is not published.
Publishing a game publishes it with its sources. If the game uses levels (attached, or whose generated <level>.lua it carries), the sheet names them and asks for a category: publishing the game also publishes each of its levels in the public levels, with its map, the models it places and their sprites. Sprites that are still private enter the public asset library, in the misc category, tagged with the project's slug; their author can reclassify them from their sheet afterwards. It is all or nothing: a level that cannot be found, a stale level (its map changed since it was compiled: recompile it), a missing model or sprite, or something not declared make the publication fail, and nothing is published. A game without levels (Lua and files) is published without these questions.
The licence follows what the game contains: CC BY as soon as one item is (a sprite, a sound, a map source, or a clone of a CC BY game), otherwise CC0 or CC BY as you choose. A CC BY version carries a credits.txt next to its files, your pseudonym first, then every credit it owes. See Licences and credits.
Whoever clones the game therefore receives its sources: its levels, their maps, models and sprites arrive in their library, ready to be modified and recompiled. Withdraw the game deletes all its published versions: the game leaves the gallery and can no longer be played or cloned. The project stays, and so do the clones already made, which no longer point to it; its public levels can then be withdrawn.
The library¶
The Library panel is the whole account, shared by all its projects. Each section has its +.
- Models: what a level places. A model has a name (a Lua key:
joueur,torche), a collision box in pixels (phy.new), an anchor from the corner of the box to the corner of the bitmap (phy.anchor), a default state, and states: each a sprite of the library, its rate in frames per image, its loop and its effects (alpha, outline, behind). The player is a model with several states (repos,av_gauche,av_droite); a torch, a model with a single one. A model placed in a level cannot be deleted. - Sprites: a single sprite, or an animated strip whose name says the number of frames (
torch_4.png). Upload them with +, or drop PNGs on the section; the workshop asks where they come from. Uploading a name you already have replaces it. A double-click shows the sprite and plays its animation, read-only. Tiles and the background are drawn in the map editor, not here. - Maps: every map of the account, with the number of levels that use it; + creates one, empty (a name, a size from 80×60 to 256×128 tiles) or from a grid (see the grid format), and opens it.
- Levels: every level of the account, attached or not, with its map; + creates one on a map you choose (a map's sheet offers New level on this map too).
- Sounds and Audio bundles: effects (
.wavconverted, or.sfx) and music (.son, edited as text in its tab and listened to there), grouped into bundles that a project attaches. See snd.
The public library¶
Public library, in the top bar, opens a tab with three shelves: Sprites, Levels and Games. Sprites and levels are browsed by category, with a count each, and searched by name, tag or author; clicking a tag shows everything carrying it. Clicking a card shows its sheet in the inspector.
The sprite categories are characters, enemies, objects, scenery, effects, interface and misc; the level categories are platformer, maze, shooter, puzzle, arcade and misc.
Copying a sprite. Its sheet shows its author, version, licence and credit; Copy to my library makes it a sprite of your own, independent: it stays if the original is withdrawn, and it can be used in a model like any other. It keeps the licence and credit of its publication. If the name is already taken, the copy takes -2, -3… before the frame count (torch-2_4).
Copying a level. Its sheet shows its image, author, map, version, licence, credits, models and sprites. Copy to my levels creates the level, its map, the models of its objects and their sprites in your account: an independent pack, which stays if the original is withdrawn. Nothing is overwritten: a model or sprite identical to one of yours is reused, and a name already taken becomes niveau1_2, joueur_2 or torche-2_4, with the references rewritten; the message after the copy lists these renames. The copied map lists, among its Sources, the level it comes from with its author's credit, so that republishing it credits them. The copy is not compiled: compile it to attach it to a project. A renamed level or model keeps its original name for the game: compiling attaches niveau1_2 under the name niveau1, writes niveau1.lua and keeps the key M.modeles.joueur, so code written for the original (require("niveau1")) works unchanged.
Games. The published games. A game's sheet gives its author, versions, licence, credits and lineage; ▶ Play in the simulator ↗ opens it in the simulator, Clone into my projects makes a new project of yours from its sources, compiles its copied levels into it and opens it. Two levels on one map bring a single copy of that map. A project's Levels folder shows exactly what the game plays: a level that is not compiled into the project is not in the game. If a level of the clone could not be compiled, the message says so: open it and compile it into the project.
Publishing a sprite. On its sheet, pick a category, add up to 8 tags (lowercase letters, digits, - and _), choose the licence its origin allows, tick the box and Publish. Your own sprite can go out under CC0 (public domain: anyone may use it without crediting you) or CC BY (your pseudonym is credited); a sprite that came from elsewhere keeps the licence it came with.
What is published is a snapshot: retouching or deleting your sprite does not change the public version. The sheet says when it has changed; Publish again makes a new version, and copies already made keep theirs. Changing only the category or tags does not create a version.
Withdrawing. The author can withdraw a publication at any time; administrators can withdraw content that does not belong there, or whose credit is false. Copies already made stay with their owners. A sprite used by a public level cannot be withdrawn: withdraw the level first.
The map editor¶
A mini Tiled: you draw 8×8 tiles there (RGBA, tile 0 is the hole) and paint them onto a world within the console's bounds (from 80×60 to 256×128 tiles, 704 tiles at most). The map is the source of the levels drawn on it, editable at will. It opens in a tab of the centre; its parts are in the tree, their sheets in the inspector, and the Tiles and Decors tabs at the bottom follow the active map.
| Tool | Key | |
|---|---|---|
| brush | B | paints the brush; Shift erases, Alt-click picks, Alt-drag takes a floating brush |
| bucket | G | fills the zone of identical tiles |
| rectangle | R | Alt: the outline only |
| line | L | |
| terrain | T | paints the chosen terrain |
| selection | S | Ctrl+C, Ctrl+X, Delete, Ctrl+D turns it into a decor |
| decor | D | places, selects, drags placed decors |
X and Y mirror the brush (free on the console: one bit per cell), Ctrl+Z and Ctrl+Y undo and redo, the wheel zooms around the cursor, a right or middle drag pans. Shift erases with every tool. Save (Ctrl+S) writes the map; if it changed elsewhere since you opened it, the editor says so and offers to reload it. Saving a map whose levels are compiled marks them recompile.
Tiles. The Tiles tab carries four bars, 8×8 then the stamps 16, 24 and 32: grids of base tile ids, the model of metatiles. The world remains a grid of 8×8 ids, and everything downstream ignores the stamps. A click takes a tile or a stamp as the brush, a double-click opens it in the pixel workshop. A stamp is composed there by painting its grid with a smaller brush, placed at 8 px steps (and cropped at the edges); the same paint engine serves the world, where the brush is a tile or a stamp of any side. Painting at the pixel is reserved for 8×8, the source of truth: retouching a base tile propagates to every stamp and to the whole map. Each tile carries a solid tag (a red corner) that prefills the collision.
Import tileset (_T.png, T ∈ {8, 16, 24, 32}) asks where the sheet comes from, then decomposes it into deduplicated base tiles, mirrors included (importing 24 also fills the 8×8 bar), and one stamp per tile above 8; its tiles are solid, untick the exceptions. A tile carries one tag at most: solid (red corner), climbable (teal corner, a ladder rung) or platform (amber corner, a cell you land on and cross from below); ticking one unticks the others. An import that would overflow the 704 is refused with the count, before writing anything; Ctrl+Z undoes it. Export grid and Import grid write and read back the map as a tile sheet and a text, the grid format.
Layers. A map is a stack of up to 8 layers, under Background layers in the tree: the eye shows or hides one, its sheet renames it, locks it, moves it forward or backward, adds one in front, deletes it. The active layer, the one selected in the tree, receives the painting, with every tool, stamp and decor brush; PageUp / PageDown change it. A hidden or locked layer refuses the painting. The eye and the lock only concern the editor: compiling flattens every layer, hidden ones included, bottom to top, each layer followed by its placed decors in their order. The Budget tab estimates the tiles after flattening: a new overlap (a plant on a wall) is one more tile on the console, out of 704.
Collision. The .mask is the collision layer OR the masks of the placed decors (mirrored as placed). Select Collision layer in the tree and paint it with any tool (Shift erases, Enter fills a selection). A cell has faces, and a face only blocks what crosses it: the brush on the layer's sheet offers solid (red, the four faces, # in the .mask), platform (the top face only, =: you land on it, you jump through it from below, ↓ + A drops through), ladder (teal, H: air with rungs, and its top cell is a floor) or faces…, four boxes for any combination (a..o in the .mask; top + left is a wall you cross from the right but not from the left). Solids fill the cell red, a face draws as a red line on its side. The masks of the placed decors show in orange and are solid: a decor placed over any other cell makes it solid in the .mask. One cell of the mask = one tile (8×8). Scenery and collision are independent: a plant can sit on a wall without being solid, an invisible ledge can be solid. Fill from tagged tiles marks every cell where a solid, climbable or platform tile is placed, on any layer (solid wins, then climbable), and the follows tagged tiles box does it while painting; with it ticked, erasing a tile also clears its collision cell, unless a tagged tile remains there on another layer. Deleting a tile from the bar leaves the collision as it is.
Terrains. A terrain paints matter (tool T): each cell picks its tile from its four neighbours of the same terrain, and edges, corners and ends come by themselves. Its sheet holds its template of 16 tiles, generated from two tiles of the brush or taken from it. Terrains are shared by the layers, and each layer keeps its own matter.
Decors. The Decors tab holds the map's library: a decor is a named grid of the map's tiles and its own collision mask. Import PNG asks where the image comes from and brings it in, undoable (Ctrl+Z): it is cut into 8×8 base tiles, deduplicated against the bar mirrors included (a cell equal to a flipped tile reuses it with the console's H/V flip bits, which cost nothing); fully transparent cells are holes, a size that is not a multiple of 8 is padded with transparency on the right and at the bottom, new tiles are not solid, and an import that would overflow the 704 is refused with the count. The mask starts from the solid tag of its tiles and is edited on the decor's sheet (click or drag toggles cells). Clicking a decor takes the decor tool (D): a click places it on the active layer, at 8 px steps; a placed decor is not painted into the layer, it stays an object that you select, drag, duplicate (Alt-drag), flip (X / Y), reorder within its layer ([ / ]) and delete (Delete), with a sheet for its position and layer. Placed decors are references: retouching a tile, renaming the decor or editing its mask updates every copy, and a decor still placed cannot be removed from the library. A selection of the active layer becomes a decor with Ctrl+D (Selection → decor): its tiles and collision leave the layer, the decor is placed at the same spot, and nothing moves on screen nor in the .mask (autotile borders around stay as they are). F flattens the selected decor onto its layer: its tiles are written there (its holes erase nothing), its mask is added to the collision, and the tiles can be edited again. To paint the tiles of a decor directly, keep it as brush and take the brush tool (B).
Sources. What the map's imports brought from elsewhere, with their licence and credit; your own work needs no source. The sheet adds and removes sources, and a map made before sources were tracked is reviewed there. The levels on the map are published under the strictest of them.
A map that levels use cannot be deleted: its sheet names them.
The level editor¶
A level opens in its own tab. Its map is shown there read-only and live: with the map open in its tab, a stroke not yet saved shows in the level at once. Only objects are edited in a level; the Tiles and Decors tabs say so and wait for a map tab.
Objects¶
The object tool places the chosen model at the pixel, feet on the clicked point; Alt snaps to the 8-pixel grid. The model to place is chosen on the Objects sheet, or by clicking a model in the library. Clicking an object selects it, dragging moves it, Delete removes it, Ctrl+Z and Ctrl+Y undo and redo; its sheet gives its position, visible, and free properties, one per line, key=value, that the game reads back in o.props.key. The grid and the eye of the collision help placing; the eye of a level is its own, independent from the map's.
A non-visible object is compiled but not created at launch: its model is in the level (a projectile, a reserve for creer() during play), instancier() skips it. An object left outside the map after the map shrank is named by the level's tab and refused when saving or compiling: move it or remove it.
Draw order. The objects of a level are drawn in the order of a list, the last one in front, exactly as the console paints gfx.sprite() calls. The Objects sheet and every object's sheet show that list, front on top like the layers in the tree: drag a row to reorder, click one to choose the object, Ctrl+Z undoes. A new object lands just below the player, so the player stays in front until you drag something above it, a foreground torch for instance. Reordering changes the M.objets[i] indices of the level's .lua.
The object's sheet also carries the effects of its model's states (alpha, outline, behind). They are saved on the model, so they hold for every level, and they are judged with the console preview in sight.
The player, and every behaviour. What an object does — the player on its pad, an enemy on patrol, a coin that counts — is written on its model, as a behaviour: a short text, one rule per line, compiled into the level's .lua. On a model's sheet, Make this the player puts the platformer preset on it (pad, walk, jump, gravity, ladders, camera); the object marked P in a level is the one whose behaviour is family player. An object can take its own copy of its model's behaviour from its sheet. The player's props (pad, speed, jump) are set like any property, key=value on the object.
Platforms. A platform cell blocks only feet coming down onto it: the player of the preset jumps through it from below and lands on it. Standing on one, ↓ + A drops through it, down to the next floor. A ladder's top cell is a platform too.
Ladders. The preset climbs the climbable cells of the map. The cell that counts is the one under the middle of its feet: ↑ grabs the ladder when that cell is climbable, ↓ when the cell just below is (climbing down from a ledge, or through the top of a ladder); a rising jump never grabs. On the ladder gravity is off, ↑ and ↓ move at the walking speed, ← → slide the player and let go as soon as the feet leave the climbable cells, A jumps off, and releasing both ↑ and ↓ while on the ground or above the last rung lets go: the player then stands on the top of the ladder, which is a floor. Two things to know when drawing a ladder: it must be at least as wide as the player's box where it crosses a floor (a 16-pixel player does not fit a one-cell hole: its sides hit the solid cells), and only the top of a ladder is a floor — a ladder that crosses a floor and continues above it leaves a hole in that floor. A ladder starting at floor level is simply walked over; ↓ takes it. A behaviour asks with ladder_here, ladder_below, platform_below; the game with niveau.grimpable(x, y) and phy.cell(x, y), in world pixels.
Compiling the level¶
The level's Compile sheet carries the palette rows by effect (opaque, translucent, additive, shadow, behind, sky ramp; 255 rows at most, row 0 is transparent), the two colours of the sky (indices reserved at the tail of the palette, top colour to bottom colour), the variants, and the projects to tick; the open project is ticked already.
Saving the level (Ctrl+S) is enough for everything that is not art: it writes <level>.lua and <level>.mask into every project that has the level, in a few milliseconds, and says which. Objects, their properties, their behaviours: save, and the game has them. One exception: when a placed model gained a state whose sprite is not in the bundle yet, the .lua would name art the game cannot load, so it is not written; the level says so, is marked recompile, and Compile puts both in step.
Compile is for the art. It saves the map if it has unsaved changes, then the level; flattens every layer of the map into the background, pushes the sprites of the placed models with the effect of their states, computes the palette, the atlas and the indexed pages in the browser, with the simulator's engine, then sends everything. If the map was saved elsewhere in the meantime, the compilation is refused and says so: compile again. The result is the level's bundle, under its name; and in each ticked project: the attachment, <level>.mask and <level>.lua. A .lua of that name written by hand is kept, and Output says so; the .mask is rewritten at every compilation.
When nothing graphic changed since the last build (the map, a sprite, an effect, the palette rows, the sky, the variants), Compile rebuilds nothing: it keeps the art, writes the two files and the preview, and says so. Compile is therefore always safe to press.
Output shows the background as the console will recompose it (clustered tiles included), the images, the number of sprites, of indices, the pool, what each project received, and the time each step took (save, build, preview, upload, reload).
The console preview¶
Console preview, in the level's toolbar or on its Compile sheet, recomposes the visible window of the world with the simulator's engine: the real tiled background, the sky, the visible objects animated, through the console's blend table. Changing an effect, the rows or the sky recomputes it; a changed effect is written into the model. This is where a "behind the background" or an outline is judged.
Publishing a level¶
The level's sheet has a Public levels section; the level must be open, since its preview is drawn by the editor. It asks for a category, up to 8 tags, the licence (CC BY as soon as its map's sources, its sprites or its own origin require it, otherwise CC0 or CC BY), the box to tick, and a category for each sprite never published; it lists the credits the level carries. Everything leaves together: the level, its map (tiles, layers, decors, collision, sources), the models it places and their sprites. A level without objects shares just its map. Sprites that are still private are also published in the public asset library, each with its category; a sprite copied from a public asset and left unchanged is not, the original is enough. Publishing saves the map and the level first. A level where an object places a missing model, where a model uses a missing sprite, or where something is not declared, cannot be published.
What is published is a snapshot: the sheet says when the level has changed since, and Publish again creates a new version; changing only the category or tags does not. The author can withdraw the level at any time, and so can administrators, unless a published game uses it: a published game is never copied without its sources, so withdraw the game first. Its sprites stay in the public asset library and copies already made stay with their owners.
The <level>.lua file¶
Generated by the compilation, never to be edited. It opens with a usage guide in comments, written with this level's own names: how to load it, what instancier() returns, the models it places and their states, their behaviours and families, and the usual moves (read a property, read the pot, change an animation, create an actor during play). It requires papyrun.lua, the behaviour runtime written into the project with it (read-only too). Then it carries the dimensions (M.largeur, M.hauteur in tiles, M.px_largeur, M.px_hauteur in pixels), the background (M.fond), the level's gravity (M.gravite, 1/256 px/frame²), the placed models (M.modeles, with anim, cadence, boucle per state — the models a behaviour spawns included), the objects (M.objets, with props, and script when the object has its own behaviour), the compiled behaviours (M.comportements, one per model and per object with its own: flags, layer, contact mask, first state, the rules as functions), and six functions:
M.charger():gfx.background,phy.map,phy.gravity, and the animations loaded once (M.anims[model][state]); returns the size in pixels.M.grimpable(x, y): true when the world pixel is on a climbable cell (a ladder):phy.cell(x, y) & phy.LADDER.M.creer(model, x, y): an actor of the model, box, anchor, position, default state; returns the actor.M.instancier(factories): the visible objects, per model, their behaviours started:objs.torche[1].ais the actor,.othe object (with.props),.animsits animations,.etatits behaviour's state.factories[model](o, x, y), if given, replacesM.creerfor that model.M.acteurskeeps every record in draw order.M.update(), from_update(): one frame of every behaviour — each object reads its pad and what the lastphy.step()produced, fires its rules — thenphy.step()and the contacts.M.joueur_updateis its old name, kept. Do not callphy.step()yourself as well.M.dessiner(), from_draw(): every object in screen coordinates (world minus camera), in the level's draw order, mirrored when facing left; the camera follows the object whose behaviour sayscamera follow.M.decharger(): frees the level's animations (phy.anim_free). The console holds 64 at a time, so a game with several levels callsold.decharger()beforenew.charger(); the actors of the old level go with it (phy.mapof the new one clears them).
The whole of a playable level, in a main.lua written by hand (the Game sheet writes a fuller one):
gfx.bundle()
local level = require("level2")
level.charger()
local objs = level.instancier()
function _update() level.update() end
function _draw() level.dessiner() end
The behaviours share the global table pot: main.lua reads it (pot.exit, pot.score) to go from one level to the next, and level.acteurs[i].etat says where each object is. Nothing in the file is random (animation phases are the game's business, see phy.anim).
The background¶
The background of a level is the world of its map. It is quantised with the sprites (a single palette, that is the whole reason it is there), then tiled: distinct 8×8 tiles by decreasing frequency, tile 0 is the hole, 704 at most; beyond that, clustering by similarity (k-means in colour, the mean of each cluster reprojected onto the indices in use): a continuous image becomes a soft mosaic instead of a brutal replacement, and the loss is counted. Outside the atlas, outside the pool.
It comes out as two pieces, <level>-fond.map and <level>-fond-tiles.png, because gfx.bundle() links a background to its group by this prefix. M.charger() writes gfx.background("<level>-fond").
Variants¶
A variant, <level>.<variant>.pal, is the same indices with other colours: day/night, a flash; no image, and gfx.palette("<level>.<name>") switches in real time.
A variant is defined on the Compile sheet by its settings (hue, saturation, lightness applied to the level's palette) and the derived .pal; the workshop re-derives the colours at each compilation, so recompiling never desynchronises its variants. The blend modes stay those of the level: a variant never changes the rows, that is its definition, and the App refuses it. Each variant counts as one palette among the project's 12.