JamChipStyle
Extends Resource. Read from the plugin's source on every build.
The project’s style: the palette, the grid and the chip’s limits every tab
starts new documents inside, and warns about when one steps outside.
docs/decisions/2026-10.md “Game developers first, a shared aesthetic,
and bring your own engine”.
One per project, named by the project setting SETTING, so a team
shares it the way it shares project.godot. A project that names none has
the built-in style, built_in, which is exactly what every tab did
before there was a style: nothing about an existing project moves until it
chooses one. The companion app, whose res:// is read-only, keeps its
choice under user:// instead and names it in its preferences,
chosen_path; the editor never reads that.
The thing a reader would get wrong: empty palettes is not “no
colours” to draw with. New work opens on the project’s default palette,
the file jamchip/pixel/default_palette names, so a project that set that
setting before styles existed keeps its palette as its style’s. Pixel
resolves it, in PixelPalette.load_default, since the kit cannot read a
Pixel palette: it reads a linked palette’s colors by name and nothing
else. A chosen style with no palette, though, holds colours to nothing:
it is the escape hatch, No Limits, and no tab warns about a colour under
it, limits_colours. No Limits is no file: SETTING
names it by NO_LIMITS, and no_limits is what that reads
as.
Properties
style_name
@export var style_name: String = ""What the style is called where it is listed. Empty is its file’s name,
display_name.
palettes
@export var palettes: Array[Resource] = []Linked .jcpalette files, all one length so a swap lines up index for
index, docs/ARCHITECTURE.md “Documents”. The first is the main one,
the colours every new sprite, tileset and font opens on; the rest are the
swaps a sprite may be shown in. Typed Resource because the kit names no
Pixel class. Empty means the project’s default palette; see the header.
Add one through link_palette, which holds the length.
grid
@export_custom(PROPERTY_HINT_RANGE, SIZE_HINT) var grid: Vector2i = DEFAULT_GRIDThe grid cell of sprites and tiles, in pixels. (1, 1) is no grid.
Held to MIN_SIZE on each side as it is set.
sprite_size
@export_custom(PROPERTY_HINT_RANGE, SIZE_HINT) var sprite_size: Vector2i = DEFAULT_SPRITE_SIZEThe size a new sprite opens at, held to a pixel the same way.
channels
@export_range(MIN_CHANNELS, MAX_CHANNELS) var channels: int = DEFAULT_CHANNELSHow many channels a song may use. A new song starts on four, or fewer.
waves
@export var waves: PackedInt32Array = PackedInt32Array()The waveforms instruments may use, as the numbers a saved sound stores
(ChiptuneOscillator.Wave). Empty is every waveform.
instruments
@export var instruments: Array[Resource] = []The style’s sound palette and its siblings: linked instrument sets, the
first the one every song and sound links, chiptune/model/song.gd. Typed
Resource as palettes is; a list so a swap set later is no new
version. docs/decisions/2026-10/29-sound-palette.md.
keys
@export var keys: Array[Dictionary] = []The keys a game’s songs move between, each {"key": "D", "scale": "dorian"} from KEY_NAMES and SCALE_WORDS, the first
the home key a new song starts in. A menu, never a lock: a song’s own key
is always its own. Empty is no home key.
motifs
@export var motifs: Array[Dictionary] = []Motifs any song may quote: {"id", "name", "rows_per_beat", "notes"}, a
note [row, degree, length] or with a fourth, alter, in semitones; a
degree is counted in scale steps from the tonic of whichever song quotes
it, so one motif sounds in that song’s own key and mode.
music_db
@export_custom(PROPERTY_HINT_RANGE, MIX_HINT) var music_db := 0.0The game’s mix: every song’s level in decibels, and every sound’s,
against each other, from MIX_MIN_DB, silence, to
MIX_MAX_DB; 0 is as made. Heard in both tabs, on every player
and in a .wav, ChiptuneGameMix, and a save is heard at once.
sounds_db
@export_custom(PROPERTY_HINT_RANGE, MIX_HINT) var sounds_db := 0.0Every sound effect’s level, the same way.
loudness
@export_range(-40.0, 0.0, 0.5) var loudness := NO_LOUDNESSThe loudness every song and sound is meant to sit at, in LUFS, or
NO_LOUDNESS for none. Kept, and read by nothing yet.
effects
@export var effects: Array = []The style’s spaces, each {"name": "cave echo", "echo": {"rows", "ms", "feedback", "damp", "level"}}: named effects a song links by name and
keeps a copy of; the record holds their shape, and their use comes with
style-sound-spaces. Data the synth renders, so the tabs, the game and a
.wav agree.
docs/decisions/2026-10/29-sound-effects.md.
Methods
built_in
static func built_in() -> JamChipStyleThe style a project has before it chooses one: JamChip’s defaults, and its five channels.
no_limits
static func no_limits() -> JamChipStyleThe escape hatch: no palette, so no tab finds a colour outside it; the chip’s most channels, every wave, no grid, and the built-in sprite size, which only seeds a new document’s.
is_no_limits
static func is_no_limits() -> boolWhether the project chose No Limits.
is_chosen
static func is_chosen() -> boolWhether this project chose a style. Only a chosen style warns about a document outside it: the built-in one is no choice anybody made.
current
static func current() -> JamChipStyleThe project’s style: the chosen file, or the built-in style when none is
named or the file named is missing or is not a style, with a line in the
log for the second, once per path. A chosen style is the live resource
out of the cache, the one every tab reads, never a private copy: a caller
reads it, and only PixelPalette.save_as_default writes to it, after the
file holds the same.
display_name
func display_name() -> Stringstyle_name, or the file’s name without its suffix when that is
empty, or the built-in name for a style with neither.
has_palette
func has_palette() -> boolWhether the style has a main palette with colours in it, rather than deferring to the project’s default palette.
main_palette
func main_palette() -> ResourceThe main palette, the first linked, or null when there is none.
instrument_set
func instrument_set() -> ResourceThe sound palette, the first linked instrument set, or null.
home_key
func home_key() -> DictionaryThe home key, the first of keys, or an empty dictionary.
palette_colors
func palette_colors() -> PackedColorArrayThe main palette’s colours, empty when the style defers.
all_colors
func all_colors() -> PackedColorArrayEvery colour in every linked palette, once: a sprite drawn in any of a style’s palettes is inside it.
palette_length
func palette_length() -> intHow many colours each of the style’s palettes has, or -1 with none.
link_palette
func link_palette(candidate: Resource, as_main := false) -> ErrorLinks candidate as the last of the style’s palettes, or in place
of the main one with as_main. Refused with
ERR_INVALID_DATA when link_refusal has a reason, which
is logged. Changes this resource only; the caller saves it.
unlink_palette
func unlink_palette(path: String) -> boolTakes the palette whose file is path out of the style’s
palettes, as style edit --unlink does. False, and nothing changed, when
none of them is that file. Changes this resource only; the caller saves
it.
length_held
func length_held(path: String) -> intThe length the palette whose file is path is held to while it is
one of the style’s palettes: the others’ length, which a swap lines up
with index for index. -1 when it is not linked or stands alone, so any
length goes. What palette edit refuses a new length by.
colors_of
static func colors_of(linked: Resource) -> PackedColorArrayThe colours of a linked palette, read by name, empty for anything else.
grid_for
func grid_for(canvas: Vector2i) -> Vector2igrid held to canvas: a cell is never larger than the
document it divides, and never under a pixel.
has_grid
func has_grid() -> boolWhether the style draws a grid: (1, 1) is none.
cell_or
func cell_or(fallback: Vector2i) -> Vector2iThe cell a new tileset or font opens on: the style’s grid, or
fallback, the tab’s own, when the style draws none.
channel_count
func channel_count() -> intchannels within what a song can hold.
allows_wave
func allows_wave(wave: int) -> boolWhether a sound may use waveform wave.
load_style
static func load_style(path: String, cache_mode := ResourceLoader.CACHE_MODE_REUSE) -> JamChipStyleThe style at path, or null for anything that is not one. The
instance Godot already has loaded, unless cache_mode asks
otherwise, so every tab reads the one current answers.
Constants
KEY_NAMES
const KEY_NAMES: Array[String] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]The pitch classes a key is named by, from C, and the seven scales by the
word a record keeps, in ChiptuneScales.SCALES’s order. Data the kit
holds so a style’s keys are checked where they are read.
SCALE_WORDS
const SCALE_WORDS: Array[String] = ["major", "dorian", "phrygian", "lydian", "mixolydian", "minor", "locrian"]The seven scales’ words, major first.
NO_LOUDNESS
const NO_LOUDNESS := 0.0No loudness target: zero LUFS is full scale, which nothing aims at.
MIX_MIN_DB
const MIX_MIN_DB := -60.0The mix’s range in decibels, Ev, 30 October 2026, “Decibels, can boost”:
0 dB plays a song or sound as made, the floor is silence, and the top is
four times as loud, which ChiptuneGameMix bends under full scale.
MIX_MAX_DB
const MIX_MAX_DB := 12.0The most a level boosts.
MIX_HINT
const MIX_HINT := "%d,%d,0.1,suffix:dB" % [MIX_MIN_DB, MIX_MAX_DB]The inspector’s hint on both levels, a tenth a step, what a migrated level is snapped to, so touching the slider does not move it.
JamChip, by GlitterSnack. MIT licence. Built from the source on every merge. Its artwork and screenshots are made with JamChip.