Skip to content

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_GRID

The 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_SIZE

The size a new sprite opens at, held to a pixel the same way.

channels

@export_range(MIN_CHANNELS, MAX_CHANNELS) var channels: int = DEFAULT_CHANNELS

How 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.0

The 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.0

Every sound effect’s level, the same way.

loudness

@export_range(-40.0, 0.0, 0.5) var loudness := NO_LOUDNESS

The 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() -> JamChipStyle

The style a project has before it chooses one: JamChip’s defaults, and its five channels.

no_limits

static func no_limits() -> JamChipStyle

The 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() -> bool

Whether the project chose No Limits.

is_chosen

static func is_chosen() -> bool

Whether 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() -> JamChipStyle

The 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() -> String

style_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() -> bool

Whether the style has a main palette with colours in it, rather than deferring to the project’s default palette.

main_palette

func main_palette() -> Resource

The main palette, the first linked, or null when there is none.

instrument_set

func instrument_set() -> Resource

The sound palette, the first linked instrument set, or null.

home_key

func home_key() -> Dictionary

The home key, the first of keys, or an empty dictionary.

palette_colors

func palette_colors() -> PackedColorArray

The main palette’s colours, empty when the style defers.

all_colors

func all_colors() -> PackedColorArray

Every colour in every linked palette, once: a sprite drawn in any of a style’s palettes is inside it.

palette_length

func palette_length() -> int

How many colours each of the style’s palettes has, or -1 with none.

func link_palette(candidate: Resource, as_main := false) -> Error

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

func unlink_palette(path: String) -> bool

Takes 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) -> int

The 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) -> PackedColorArray

The colours of a linked palette, read by name, empty for anything else.

grid_for

func grid_for(canvas: Vector2i) -> Vector2i

grid held to canvas: a cell is never larger than the document it divides, and never under a pixel.

has_grid

func has_grid() -> bool

Whether the style draws a grid: (1, 1) is none.

cell_or

func cell_or(fallback: Vector2i) -> Vector2i

The 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() -> int

channels within what a song can hold.

allows_wave

func allows_wave(wave: int) -> bool

Whether a sound may use waveform wave.

load_style

static func load_style(path: String, cache_mode := ResourceLoader.CACHE_MODE_REUSE) -> JamChipStyle

The 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.0

No loudness target: zero LUFS is full scale, which nothing aims at.

MIX_MIN_DB

const MIX_MIN_DB := -60.0

The 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.0

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