Dev blog 2026-10-22 · Keel 0.5.3 development

Art by script, colour by one file

Ship art comes from scripts, and every colour comes from one file.

Keel has no hired artist. It has two Python scripts that make sprites, one GDScript file that holds every colour, and a rule that no screen may invent a hue. This post tours that pipeline and says plainly which ships the scripts draw and which they do not.

The faction art for classes 1 to 5, the Haven station, and the backgrounds shipped in 0.4.1. The class 6 hulls and the three Titan sprites are coming in 0.5.3.

Keel in play on Layer 1. Six Carriers on a hex map engage two Breaker patrols. The fleet list is on the left, contacts on the right, and orders and powers along the bottom. Faction sprites sit on the fleet cards, the contact cards, and the map.
The look today, from a capture on the pitch page. A dev account took it, as the Dev tools button shows. Six Carriers face two Breaker patrols over a faint nebula tile.
The problem

A ship has to say whose it is at 20 pixels.

In the first alpha a ship was a triangle on a card, and the whole interface was ice cyan on navy.

That does not survive a real fleet. Four factions field five hull classes today, and a sixth is coming in 0.5.3. A faction has to read on a fleet card at 20 pixels, in a contact tile at 28, and on the map at 28 to 42, over any nebula. A colour also has to keep one meaning, so that gold stays your target and red stays a threat.

There was a second problem. The rules we wrote at the start say there is no hired artist and that art stays CC0 packs and placeholders. So the art had to come from code, from free packs, or from both.

An early alpha screen. The fleet list on the left shows Haven as a cyan square and two Patrols as small cyan triangles. An Inventory panel with a stash list covers the middle, and the interface edges are cyan on near-black.
An early alpha capture from 2026-10-18, before the faction art of 2026-10-19. Ships are plain triangles and the interface is cyan on navy.
What we wanted instead

Three rules for a game that draws itself.

They come from the colour bible, approved under D58 on 2026-10-18, and from the first art pass the next day.

One hue, one meaning

Gold is your target and nothing else. Sea-glass is yours. Flare is hostile. Damage types, factions, and rarities also carry a glyph or a notch, so colour is never the only signal.

A faction reads by shape and colour

A pilot should name the faction before reading the card. Colour does most of the work. A silhouette, a badge, or a backdrop shape does the rest.

Colours come from one file

palette.gd holds every token. The project rule is that colours come only from the palette. A new colour goes into the bible first, with its one meaning, and then into that file.

What we built

Two scripts make the sprites. One file holds the colours.

make_faction_art.py draws hulls, Titans, and icons with Pillow. make_pack_art.py recolours outside CC0 art for the older hulls, the station, and the backgrounds. palette.gd holds the colours, and art.gd loads the sprites.

27Ship sprite files: 24 hulls, 6 classes for 4 factions, and 3 Titans. The 4 class 6 hulls and the 3 Titans are coming in 0.5.3.
44Faction icons: 11 kinds of module in 4 faction shapes.
256 pxSprite size. The script draws at 1024 px and shrinks it at the end.
134Lines in the palette file, which holds 88 hex values.
Two generations

Classes 1 to 5 are pack art. Class 6 is ours.

The script came first. On 2026-10-19 it drew five hulls for each faction. Later that day decision D74 allowed outside art, and make_pack_art.py replaced those hulls with recoloured pieces from a CC0 space shooter pack by Kenney.

The top row is what the build holds. The bottom row is what the script draws for the same six classes, rendered for this post. Its sixth hull is the file we ship for class 6.

That is why the Raider row changes style at the end. Class 6 follows the script. Classes 1 to 5 follow the pack.

Two rows of six Raider ships from Patrol to Dreadnought. The top row shows five flat orange and white hulls and then a dark brown hull with orange lines. The bottom row shows six dark brown hulls with saw-tooth wings that grow wider and gain more teeth with each class.
Raiders, Patrol to Dreadnought. Top row: the files in the build, classes 1 to 5 from the pack and class 6 from the script, coming in 0.5.3. Bottom row: the script's own six hulls, drawn in memory for this post. Contact sheet composed with PIL.
A faction by shape

On class 6 a faction is a silhouette, not only a tint.

On classes 1 to 5 the four factions share five shapes and differ by colour. The script gives each faction its own outline, and class 6 is the first shipped hull to use it.

Each class step adds detail. A Raider gains a saw-tooth on each wing, from two on a Patrol to seven on a Dreadnought. A Sentry gains halo rings, one to three. A Breaker gains side pods, one to three on each side. A Human gains engines, two and then four.

Two rows of four ships, left to right Raiders, Sentries, Breakers, Humans. The top row shows the same hexagonal Carrier hull in rust, ice, orchid, and pewter. The bottom row shows four different Dreadnoughts: a winged rust hull, a pale needle inside halo rings, a violet armoured hexagon with side pods, and a gunmetal delta.
Top row: the Carrier of each faction, pack art in four colours. Bottom row: the Dreadnought of each faction, drawn by the script, coming in 0.5.3. Contact sheet composed with PIL.
RaidersRust hull, orange glow. Forked prongs and saw-tooth swept wings.
SentriesPale hull, cyan glow. A slender needle with halo rings.
BreakersDark violet hull, magenta vents. Blunt stacked armour plates.
HumansGunmetal hull, pewter running lights. A utilitarian delta with twin engines.

The one-line looks are the script's own description of each faction.

Titans, coming in 0.5.3

A Titan is a Dreadnought under a ring.

A Titan sprite is its faction's Dreadnought hull under a wide ring in the faction colour, with a soft glow. Raiders, Sentries, and Breakers have one each. The Humans have none.

All seven new sprites are in the client assets. We found no client code that loads a Titan sprite yet, so we have no capture of one in play.

Two rows of three ships, left to right Raiders, Sentries, Breakers. The top row shows each Dreadnought hull inside a wide ring in the faction colour. The bottom row shows the same three hulls without a ring.
Top row: the three Titan sprites. Bottom row: the Dreadnought hulls they are built on. Both coming in 0.5.3. Contact sheet composed with PIL.
As a pilot sees it

One sprite, five places.

The server sends a faction and a class number. The client picks the picture and the size. It never receives art.

  1. Fleet cardA 20 pixel sprite beside the ship name. It dims to half strength while the ship is berthed.
  2. The map28 pixels for a Patrol, 3.5 more for each class, 42 for a Carrier. It turns to the way it flies and sits on a faint moss disc that says it is yours.
  3. Contact tileA 28 pixel sprite on a disc in the relation colour. A hostile one is drawn nose down.
  4. FittingA module icon wears its faction shape, a corner badge, and a row of rarity notches.
  5. Login pageHaven and four ships drift behind the sign-in, one for each faction, 66 to 102 pixels wide.
The map

A hex, a nebula, and a station.

The grid is drawn in code. The nebula and the starfield are faint CC0 tiles under it, one nebula tile for each layer and one for the Rifts. Haven is a CC0 station piece with its blue panels shifted to sea-glass, so it does not read as a friendly blue contact. The belt rocks are CC0 too.

Each hex is drawn at nine tenths of full size, so the gaps read as lines. The centre hex has a raised fill and a Pearl edge.

Layer 1 docked at Haven. A pale station with eight green solar panels sits in a hex near the middle of a dark hex grid over a starfield. Belt rocks and two blocks of incursion hexes, one cyan and one violet, are drawn on the grid. Six Carriers are listed on the left.
Layer 1 at Haven, from the itch store set, taken on a dev account. The station, the rocks, and the nebula are pack art. The hexes and their colours are drawn in code.
Icons

The same module in four faction shapes.

Each module icon exists in four versions. A Raider icon sits on a 16 point star, a Sentry icon in a ring, a Breaker icon in a hexagon, and a Human icon in a square. The same shape sits in the corner as a badge.

The script starts from the existing slot icon, re-hues its cyan accent to the faction colour, and keeps the lightness. A near-grey faction keeps its low saturation, so a Human accent never reads as the Rare blue.

A grid of twelve module icons. The rows are a weapon, a shield, and a drive. The columns are Raiders on a rust star, Sentries in a pale ring, Breakers in a violet hexagon, and Humans in a grey square. Each has a small matching badge in the top left corner.
Weapon, shield, and drive icons for Raiders, Sentries, Breakers, and Humans, left to right. Contact sheet composed with PIL from client/assets/icons.
The early alpha Inventory screen with six slot frames. The frames are dark squares, some edged in gold or violet, with a cyan base line. None carries a faction shape.
Before: an early alpha capture from 2026-10-18. The slot frames carry no faction shape.
The Fitting screen for a Raider Carrier. Weapons, systems, and hull fittings sit in rust-edged wells. Each icon sits on a faction star and carries four small rarity notches in its top left corner. A power grid bar and a stat card are on the right.
After: the Fitting screen from the itch store set, taken on a dev account with a Raider Carrier. Each icon sits on the Raider star, with its rarity notches above.
Under the hood

Two scripts, one palette, one loader.

Five files hold the pipeline. make_faction_art.py is 450 lines, make_pack_art.py 158, palette.gd 134, art.gd 86, and hex_grid.gd 214. The code that draws with them sits in main.gd, ship_tile.gd, and the panels.

Generatemake_faction_art.py draws hulls, Titans, faction icons, and the app icon with Pillow. make_pack_art.py recolours Kenney pieces into classes 1 to 5, Haven, and the rocks, and cuts the backgrounds.
Colourpalette.gd is plain constants: ten inks, the roles, damage, faction, rarity, layer tints, relation colours, and materials. A few helpers return a colour or a box.
Loadart.gd builds the path, caches the texture, and falls back when a file is missing. The map, the cards, Fitting, and the login page call it.

One sprite, as a call chain

  1. DrawThe faction's function builds a mirrored hull from polygons on a 1024 pixel canvas. The hull scale starts at 0.62 and grows 0.09 a class.DRAW, ship_raider, mirror, plate
  2. FitThe Dreadnought and the Titans are drawn with their geometry at 0.8 about the centre. A last check shrinks anything that still overflows.zoomed, Zoom, fit
  3. LightA rim of light on the top left, a shade on the bottom right, a dark outline, then the shrink to 256 pixels.edge_light, finish
  4. SaveThe PNG is named by faction and class, and Godot writes its import file beside it.main
  5. LoadThe client builds the path from a faction and a class, clamps the class, and falls back if the file is missing.Art.ship, Art._load
  6. PaintThe map, the cards, and the login page choose the size and the turn, and draw the texture._paint_ship, _draw_fleet_card

The main functions

ship_raider, ship_sentry, ship_breaker, ship_humanmake_faction_art.py

One function for each faction. Each takes a class from 0 to 5 and builds a symmetric hull. Detail appears with the class: wing teeth, halo rings, side pods, engines, and a hatch from class 5.

mirror, plate, glowmake_faction_art.py

The three drawing helpers. Mirror turns right-half points into a full outline. Plate fills a polygon dark, an inset copy light, and strokes an edge. Glow stacks six blurred circles for an engine.

zoomed, Zoom, fitmake_faction_art.py

The Dreadnought is wider than the canvas. Zoom scales the geometry before it is drawn, because a later resize cannot restore pixels the canvas already clipped. Fit is the safety net.

edge_light, finishmake_faction_art.py

The rim light and shade, with strengths of 0.55 and 0.6, then a dark outline at 0.75 and the downsample. They make a flat cut-out read as a lit solid on any nebula.

titan_artmake_faction_art.py

The faction's class 6 hull, a wide ellipse ring in the faction accent at 0.47 of the canvas, and a soft glow.

icon_variant, backdrop, badge, recolourmake_faction_art.py

Build a faction icon from a slot icon: re-hue the cyan, draw the star, ring, hexagon, or square behind it, and add the corner badge at 24 percent of the icon.

app_iconmake_faction_art.py

The window and executable icon: a Pearl hex ring around a keel on Ink. Its four colours each equal a palette token. The icon file carries seven sizes, 16 to 256 pixels.

ships, _recolour, _sea_panelsmake_pack_art.py

Turn five Kenney enemy shapes into classes 1 to 5, nose up, in each faction's colours, and shift the station's blue panels to sea-glass.

ship, icon, nebula, tileart.gd

The whole client side of the pipeline. A ship falls back from its file to the shared sprite, and an icon falls back to the plain slot icon. A rift or a Titan pocket borrows a nebula tile.

box, faction, hullpalette.gd

Small helpers. Box makes a rounded panel style from a fill and an edge. Faction returns a hue with an Ink 300 fallback. Hull is Moss at 30 percent or more and Flare below.

_cell_style, _sync_back, _draw_backmain.gd

The hex colours, and the cached layer behind the ships. The settled map is one triangle array and one multiline, redrawn only when its key changes.

Four choices we are happy with

Colours are constants.The palette is plain values in one file. A colour change is a one-line diff, and every screen that reads the token follows.
Every load has a fallback.A missing ship file falls back to a shared sprite, then to a circle on the map or a glyph on a card. A missing icon falls back to the plain slot icon. Each caller checks for a missing texture.
Draw big, shrink at the end.The script draws at four times the size, adds its light and shade there, and downsamples last. Edges stay clean at 20 pixels.
The settled map is cached.The back layer redraws only when the layer, the layout tag, a count, a danger, contact, or own-ship set, a storm warning, or Haven changes. Per frame the client redraws only hexes that pulse, flicker, or carry your ships.
The client side

The server owns the facts. The client owns the pixels.

  • The server sends a faction and a class number. art.gd is the only file that builds the path to a faction ship, so no caller names a file.
  • hex_grid.gd draws nothing. It holds the math: pointy-top hexes of 36 pixels, cube coordinates, neighbours, and a path.
  • The panel look lives in _ui_theme in main.gd. Buttons are Ink 800 with an Ink 600 edge and a Pearl edge on hover. Panels are Ink 900 at 96 percent with a radius of 8 pixels. Lighter means higher, as the bible says.
  • The type scale is four sizes in palette.gd: 18, 14, 13, and 11 base pixels. The palette comment says nothing a player reads is under 11. Some map labels are drawn at 10, and the rarity notches at 7.
What the code looks like in numbers
88Hex values in palette.gd
891Client lines that mention a palette token
25 of 33Client scripts that load the palette
8Layer tints: six layers, the tutorial, and the Rifts
What the tests prove

What is pinned, and what is not.

Almost nothing here has a test. The client self-test and the unit tests run on every commit, and neither looks at a sprite or a colour. This list is short on purpose.

Pinned

  • The client self-test: the hex math. Every cell of a 15 by 15 block comes back to itself through its centre and the pointer lookup, and a path from 8,8 to 8,5 takes three steps.
  • The client self-test: the Rift box. The grid accepts 28,28 and refuses 29,1.
  • One check we ran for this post, which is not a test in the repo. Called with the class argument, the script reproduces the four class 6 files and the three Titan files exactly.

Not pinned, or thin

  • No test loads a sprite or counts the files. A missing sprite falls back without a sound, so nothing would fail.
  • No test or hook looks for a colour written outside palette.gd. A search for this post found no hex colour constant in the client scripts outside the palette, and one help-panel line that types two hex values into a text string. They equal Ink 100 and Azure today.
  • The script's accents are typed in again and are not read from the palette. The four faction accents match the palette today. Nothing keeps them matched.
  • As committed, the script's main call for the Dreadnought leaves out the class argument, so a full run stops with a TypeError. It would first overwrite the five Raider hulls of classes 1 to 5, which differ from the shipped pack art in about 26,000 to 40,000 of 65,536 pixels each. We found this by calling its functions in memory. We did not run it.
  • No client code loads the Titan sprites. On the map the size ramp stops at the Carrier, so a Dreadnought is drawn at 42 pixels like a Carrier.
  • Two default fonts are named. The project file names Chakra Petch, and the interface theme in main.gd names Segoe UI and then Arial. We did not check which one each screen ends up with.
  • The bible's swatch page still carries the label Proposal, not applied, and the text file's title still says proposal, though its status line says approved on 2026-10-18.
  • The bible lists the faction badge shapes as a notched chevron, a ringed dot, a split diamond, and a plain hex. The icon backdrops are a star, a ring, a hexagon, and a square. We did not find where the two are reconciled.
  • We found no measurement of how readable the sprites are. Nothing in the repo counts a mix-up between two ships.
What we learned and what is next

Small lessons from a game that draws itself.

Gold means your target.The first Human running lights were gold. The stabilization pass of 2026-10-19 moved them to the bible's pewter, because gold has one job. The same pass added the rim light and the shade, and widened the Sentry needle, which read as a hairline at map size.
Scale before you clip.The first Dreadnought and Titan files ran to the edge of the canvas on every side. They were fitted after drawing, which cannot bring back pixels the canvas already lost. A day later the script scaled the geometry before drawing, at 0.8 about the centre.
Say which art is which.The decision log says to draw class 6 like classes 1 to 5. The script holds one version of those hulls and the game holds another, from the pack that replaced it. Class 6 follows the script, so the ladder shows two hands. We would rather say so than hide it.

Where we would push next

  • Treat every sprite as provisional. The script's own header calls the art procedural and provisional, and we agree.
  • Fix the Dreadnought call, and make the script write only the files it owns, so a run cannot overwrite pack art.
  • Load the Titan sprites, so a Titan reads as a Titan on the map.
  • Check the palette rule in a test and not by a search, and read the script's accents from the same values.
  • Decide whether a Dreadnought should be bigger than a Carrier on the map.
  • Bring the bible's labels up to date, and reconcile the badge shapes with the icon backdrops.
Join the open alpha

Tell us which two ships look alike.

Keel is free to play on PC, and the open alpha is where the art lives. Find us in the Keel Discord, tell us what the art did to your evening, and file a report from inside the game when something looks wrong.

Players whose reports become fixes are credited in game. Bring a mixed fleet, zoom the map in, and tell us whether you can name every faction at a glance.