Dev blog 2026-10-22 · Keel 0.5.3 development

Three waves, one warning

Sixty seconds of warning, then three waves of ships.

We rebuilt the incursion so that you see it coming, fight it in three waves, and are paid for what you did. This post walks the loop a pilot lives, the numbers behind it, the code that runs it, and the parts that are still thin.

Incursions have run since 2026-10-18, first on Layer 2, and the live build is 0.5.2.1. The warning, the waves, the new ship counts, PvE Salt, and the Keel Plate are coming in 0.5.3.

The Layer 2 map with two incursions. Each is a group of hexagons marked with a cross and labelled INCURSION, an orange group near the centre and a purple group at the upper right. A fleet of six Carriers is listed on the left, and the top bar reads two incursions.
Layer 2 with two incursions, the most a layer holds. A store capture from the 0.5.2 build, so it has no warning and no waves yet. The fleet and the wallet are dev-filled.
The problem

A raid with no warning and little in it.

Until 0.5.3, an incursion landed in one tick and held two ships in every sector.

Every five minutes the server rolls each layer from Layer 2 up. On a hit, the whole incursion was in the world at once. The first sign was the incursion count in the top bar and a sound.

Two ships a sector was the bigger problem. No sector may hold more than 3, 4, 6, 8, or 10 ships on Layers 2 to 6, so an incursion sector held fewer ships than an ordinary one could. A tester's report pointed it out, and we slated the fix for 0.5.3.

It was also one fight with one payout. There was nothing to prepare for, and nothing to escalate.

The Layer 6 map. An orange group of hexagons marked with crosses sits at the upper left, and a blue group at the lower right. The top bar reads two incursions. Raider Carriers and Cruisers fill the contact list on the right.
Layer 6 in a 0.5.2 store capture, drawn the same way as today. An orange incursion at the upper left and a blue one at the lower right. In that build each of their sectors held two ships.
What we wanted instead

See it, fight it, get paid for it.

The shape came from spec items INC-12 and ECO-14, and from that density report. We wanted an event a fleet can read, answer, and be paid for.

You see it coming

A banner and an outline on the map, 60 seconds before the first ship. A fleet has time to leave, arrive, or ignore it on purpose.

It has a shape

A swarm, then a battle line, then a command group. Each wave flies bigger hulls where the layer has them, and the next one does not wait for you to be ready.

You are paid for damage

Nothing pays before the last ship dies. Then everyone who did at least 5 percent of the damage is paid, so a last-hit snipe takes nothing.

What we built

Announced, fought in three waves, paid once.

A layer from Layer 2 up, or a Rift, holds at most two incursions at a time. Each is one faction on a connected set of at least four sectors, and each sector holds a number of ships that depends on the layer.

60 sWarning before the first ship appears.
3Waves: a screen swarm, a battle line, and a command group.
300 sTimer on waves one and two. The third wave has none.
5 percentShare of the damage that gets you paid.
Layer 26 ships a sector, split 3, 2, 1. Patrols and Frigates, then Frigates, then Frigates.Salt 1
Layer 38 ships a sector, split 4, 3, 1. Patrols and Frigates, then Destroyers, then Destroyers.Salt 1
Layer 412 ships a sector, split 6, 4, 2. Patrols and Frigates, then Destroyers and Cruisers, then Carriers. A Keel Plate.Salt 2
Layer 516 ships a sector, split 8, 5, 3. Frigates, then Destroyers and Cruisers, then Carriers. A Keel Plate.Salt 3
Layer 620 ships a sector, split 10, 7, 3. Destroyers, then Destroyers and Cruisers, then Carriers. A Keel Plate.Salt 4

The row is twice a full ordinary sector on that layer. The waves take about half, a third, and a sixth of it, and the first wave takes the remainder so the three add up. Layer 1 keeps its small training incursion, with no waves, no warning, and no Salt.

What each wave flies

A small Raider Patrol hull, orange wings around a pale cockpit.Wave 1, the screen swarmAbout half the sector's ships. Patrols and Frigates where the layer flies them, the nearest class it does fly where it does not.
A Raider Cruiser hull, a pale wedge with a dark core between orange wings.Wave 2, the battle lineAbout a third. Destroyers and Cruisers, from the layer's own class mix.
A Raider Carrier hull, a long pale spine inside an orange hexagon.Wave 3, the command groupAbout a sixth. Carriers, on the layers that fly any. The incursion ends when the last of them dies.

Sprites show Raider hulls. Each incursion picks Raiders, Sentries, or Breakers at random. Every ship flies the layer's gear one grade step up, two ranks higher, and ten quality better.

What changes in 0.5.3

Live in 0.5.2.1

  • No warning. The ships land the tick the check rolls.
  • Two ships in every sector, on every layer.
  • One fight and one payout.
  • No Salt from a clear, and no Keel Plate.

Coming in 0.5.3

  • A 60 second warning with a banner and an outlined set of sectors.
  • 6, 8, 12, 16, and 20 ships a sector on Layers 2 to 6, in three waves with a 5 minute timer.
  • PvE Salt to everyone at 5 percent or more: 1, 1, 2, 3, and 4 on Layers 2 to 6.
  • One Keel Plate to everyone at 5 percent or more on Layer 4 and deeper.
As a pilot lives it

A minute to get ready, then three fights.

The server owns the countdown, the sectors, the ships, and the payout. The client draws a banner and an outline, and writes the lines the server sends.

  1. The rollEvery 300 seconds the server rolls each layer from Layer 2 to Layer 6, and each Rift, at 40 percent. A layer already holding two, warned ones included, is skipped. So is a layer with no room for four connected sectors.
  2. The warningPilots on that layer get a banner, and the chosen sectors are outlined in the faction's colour with a countdown. The ships are not there yet, and no second incursion can take those sectors.
  3. Wave oneAt zero the swarm starts to appear, at most 24 ships a tick across all its sectors. The map labels the set INCURSION. The ships hold their sectors and do not chase or come back.
  4. The next waveClear the wave, or wait 300 seconds, and the next wave launches and the log says so. Ships you left alive stay. The third wave has no timer.
  5. The clearWhen the last ship of wave three dies, the incursion pays once and the layer has a free slot again. Nothing paid before that.
Banner! Raider incursion in 42 s near 5,5
TooltipRaider incursion, 12 ships.
LogWave 2 of the Raider incursion has launched.
LogIncursion cleared. Human Weapon copy, class 1, to your blueprints.
LogIncursion cleared. +3 Salt.
LogIncursion cleared. +1 Keel Plate.

The wording is the game's own. The faction, the seconds, the sector, the ship count, and the piece are filled in by the server, and the numbers here are examples from a Layer 5 clear. The banner, the wave line, the Salt line, and the Keel Plate line are coming in 0.5.3.

Clearing it

One incursion, not a sector at a time

The game does not track sectors as cleared. It counts every live ship in every sector of the incursion, and a wave is cleared when none are left anywhere. Contribution is the shield and hull damage you deal to its ships, added over all three waves.

Everyone at 5 percent or more is paid. The top contributor gets a Human blueprint copy: a hull copy of class 3 or better one time in four, a class 1 module copy otherwise. They also get a one in ten chance at an original. Everyone else over the line gets a class 1 module copy.

The pay

Salt and Plates to every contributor

Each pilot over the line gets the full Salt for the layer, not a share of it, so a crowd does not dilute it. On Layer 4 and deeper each also gets one Keel Plate, with no roll. A Dreadnought bill takes Keel Plates, and Dreadnoughts are coming in 0.5.3 with a post of their own.

From Layer 3 up a pot of Notches is also split by contribution, 10, 15, 23, and 34 on Layers 3 to 6. Layer 1's training incursion pays 10 XP and one class 1 copy, and no Salt.

Inside a Rift the same loop runs bigger, and pays more. The Rift post has those numbers. New to all this? Help has an Incursions tutorial that runs a practice incursion in two private sectors.

Under the hood

One row, one tick loop, one payout.

About 284 lines in world.py, 74 lines of constants and helpers in rules.py, 16 lines of class bands in npc_gear.py, and two small functions in snapindex.py. An incursion is one dict, and a tick loop walks it through its waves.

The rowOne dict in world.incursions: id, layer, faction, sectors, a damage table, the wave number, the wave timer, and a queue of ships still to spawn. It is saved with the world.
The rulesrules.py holds the constants and the helpers for ships a sector, the wave split, and Salt. Balance rows are in balance/tuning.csv. The wave shares and the spawn count a tick are code constants, not rows.
The viewsnapindex.py puts the layer's incursion rows and its soonest warning into each pilot's snapshot. The client draws from that and decides nothing.

One incursion, as a call chain

  1. RollEvery 300 seconds, 40 percent a layer and Rift. It warns and spawns nothing._tick_incursions, _warn_incursion
  2. PlanA random growth of four or more connected sectors that avoids Haven, its neighbours, wormholes, and other incursions. A faction is drawn._plan_incursion
  3. Count downThe plan waits 60 seconds. At zero the sectors are checked against the layer's current shape, and the plan is dropped if fewer than four remain._tick_incursion_warnings, _spawn_incursion
  4. LaunchA wave is queued as sector and class pairs, then 24 ships a tick go into the world._launch_wave, _drain_wave
  5. AdvanceA clear or 300 seconds drops the dead and launches the next wave._advance_wave
  6. PayEach hit adds damage to the table. When the last ship of wave three is dead, one function pays everything._credit_incursion, _pay_incursion

The main functions

_warn_incursionworld.py

Plans an incursion now and parks it for 60 seconds. It takes an id and holds its sectors, and the snapshot reads it. Nothing is in the world yet.

_drain_waveworld.py

Pops queued ships into the world, up to a limit or all of them. The tick passes 24. Dev spawns and tests pass no limit. Each ship carries the incursion's id.

_pay_incursionworld.py

The whole payout in one function: the 5 percent line, the top contributor's copy and hull roll, a copy for each other contributor, the original roll, Salt, Keel Plates, and the Notch pot. It returns early when nobody is over the line.

incursion_wavesrules.py

Splits a sector's row into three counts of about half, a third, and a sixth. The first wave takes the remainder, so the three add up.

wave_classnpc_gear.py

Picks a hull class for a wave from the layer's own class weights, limited to the wave's band. Where the layer flies nothing in the band, it takes the nearest class it does fly.

_warning_rowsmain.gd

Reads the snapshot's warning, keeps only well-formed sectors, and feeds both the banner and the outline.

Choices we are happy with

A queue, not a spawnA wave is queued, then drained 24 ships a tick, so a Layer 6 wave cannot stall one server step. The incursion cannot count as cleared while the queue holds ships. A test pins both.
Warn first, check againThe warning stores a plan, and the landing checks the sectors again. A storm in that minute cannot put ships on a wall, and a plan left with fewer than four sectors is dropped.
Old rows still payAn incursion saved before 0.5.3 has no wave fields. It pays on its first clear instead of waiting for waves that will never come.
Damage is the only creditThe pay follows damage dealt and nothing else. A last-hit snipe gets nothing extra, and the payout reads one table with no timing rules.
The client side

The server counts. The client draws.

  • main.gd reads the seconds from each snapshot and shows the banner. It has no countdown of its own.
  • The outline and the INCURSION label take their colour from palette.gd, through the faction table.
  • The snapshot carries only the warning for the pilot's own layer, so a pilot elsewhere sees no banner.
  • Hits, damage credit, and the payout are all in the server. The client cannot add to a contribution.
What the code looks like in numbers
284Lines from _incursion_blocked to _pay_incursion in world.py.
74Lines of incursion constants and helpers in rules.py.
22Tests in the IncursionTest class.
24Ships a tick that a launching wave puts into the world.
What the tests prove

What is pinned, and what is not.

The unit tests build a world in memory and tick it by hand, and they run in the pre-commit hook. Two end-to-end checks run a real server. We have not counted how many times anything passed, only what each test holds.

Pinned

  • test_every_layer_row_fills_the_sector_over_three_waves: 6, 8, 12, 16, and 20 ships a sector, in three waves that add up to the row.
  • test_waves_launch_on_a_clear_in_order_and_pay_only_after_the_last: wave order, nothing paid before wave three, and the classes in each band.
  • test_the_next_wave_launches_after_five_minutes_with_ships_left: the timer, and that wave one's survivors stay.
  • test_pilots_get_a_warning_before_the_ships_appear: 60 seconds, the warned sectors held, the right layer only, and the ships landing at zero.
  • test_a_clear_pays_salt_to_contributors_over_five_percent_once: 3 Salt on Layer 5 to each pilot over the line, none under it, and never twice.
  • test_a_deep_incursion_clear_pays_a_keel_plate_to_each_contributor_over_five_percent: a Plate on Layer 4 for the 96 percent pilot and none for the 4 percent pilot, and none on Layer 3.
  • test_a_legacy_saved_incursion_with_no_waves_or_queue_pays_at_once: rows saved before 0.5.3 still pay.

Not pinned, or thin

  • No check runs a full three-wave incursion over the wire. playtest_check sets the waves to 1 on the incursions it spawns, because its loops clear and pay after one. alpha_check only checks that two accounts see an incursion and are credited.
  • On Layer 2 every wave after the first flies Frigates, and on Layer 3 every wave after the first flies Destroyers. There the waves differ only in count.
  • The timer is pinned from wave one to wave two. Nothing ticks a third wave for 300 seconds to show it has none.
  • The client self-test pins the banner text and the sector parsing. It does not pin the outline drawing.
  • The snapshot carries only the soonest warning. With two warned incursions on a layer, a pilot sees one.
  • The design text says contribution counts damage and time. The code counts damage only.
  • This post quotes no load or timing measurement. The wave shares, 50, 33, and 17 percent, are stand-ins, and so is the 1 in 4 hull chance.
What we learned and what is next

The rule said double. The code said two.

A rule and a constant driftedThe design said double density. One constant put two ships in every sector. A tester's report caught it, and we replaced the constant with a row per layer, twice a full ordinary sector.
Waves broke a test assumptionOur end-to-end loops assumed that a clear pays. With three waves it does not, so the playtest check now sets the waves to 1. That keeps the loop testing the payout, and it means the check exercises less than the real thing.
The size of a wave is a costA full Layer 6 incursion can reach 14 sectors of 20 ships, which is 280 over three waves. We spread each launch over ticks instead of spawning it in one.

Where we would push next

None of these has a version yet. They are not in a build.

  • Watch real 0.5.3 incursions and tune the wave shares and the 5 minute timer with testers.
  • Run load_check with its incursion option on Layer 6 and on Rift 6, against the 25 millisecond budget for the 50 millisecond step.
  • Give NPC waves hull lines. The spec names Lances, Anchors, and Tenders, and today the bands are by class only.
  • Warn about every pending incursion on a layer, and settle whether contribution should count time as the design text says.
Join the open alpha

Come and meet the first wave.

Keel is free to play on PC, and the open alpha is where incursions live. Find us in the Keel Discord, tell us what an incursion 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. Once 0.5.3 is out, take two friends to a Layer 4 incursion and tell us whether five minutes a wave feels right.