Dev blog 2026-10-22 · Keel 0.5.3 development

One fleet, four wings

Split a fleet into wings and give each its own orders.

We replaced the F1 to F4 control groups with four wings, A to D, and a Main wing for every ship you leave out. Each wing keeps its own stance, formation and anchor. This post covers why, how it works, the code, and a short rule about logging out that comes with it.

Wings and the logout berth are coming in 0.5.3. The live build is 0.5.2.1, which still has the old control groups.

M stackedactiveA in a lineB pinned, focusHold set on B
A·3 F 100% B·2 G 64% !3 ◎ M·3* F 100%
Active ship Wing A Wing B Main Hostile Pinned sector Focus target
Schematic drawn for this post, not a capture. Wing A follows in a line, two Main ships stack on the active ship, and wing B holds a sector seven hexes away and fights it out. Wings C and D are empty, so they have no chip. The chip text follows the format the client builds.
The problem

F1 to F4 remembered who was selected, and nothing else.

In 0.5.2.1 a control group is a list of ship ids. Ctrl and F1 to F4 stores the list, and F1 to F4 picks it.

That was all it did. A group had no stance, no formation and no order of its own, and one ship could sit in two groups. Taking control of a grouped ship dropped it from the current pick without a word, so the group you built was not always the group your next order reached.

A report from the alpha asked for a fleet that splits into groups you control one at a time. We read the report against the code and wrote down what failed.

  • One fleet, one anchor. Every formation, follow and focus hung off the single active ship. A flank sent to the next sector had no formation and no stance that survived your next click on all escorts.
  • Two enemy stacks. A left click on stack A put every escort in your sector on it. A click on stack B replaced those orders, and when B's target died the whole fleet flew back to your ship, even with A still alive next door.
  • A clamped mate. A Clamped escort stayed put while the rest followed you away. Nothing sent help.
  • A focus that ended too fast. When a focus target died the order dropped back to Follow at once, even with more hostiles in the sector. A busy sector shuttled ships between focus, follow and a neighbour's fight.
  • One formation for everyone. Changing it moved every escort, including a ship that was meant to hold a sector.
Two Patrol ships in a small fleet column on the left, one marked picked. Along the bottom, the command bar shows orders, stances, formations and a GROUP row with chips F1 to F4 and Auto, and the label at the right reads 1 picked.
The old command bar in an early alpha capture from before wings. The GROUP chips F1 to F4 sit at the end of the stance row and the label reads 1 picked. This is the bar wings replace.
What we wanted instead

Three rules before any code.

The design note put ten questions to the people who decide, and every recommended answer was accepted. Three of them shaped everything else.

Default play does not change

With no wing picked, an order reaches every escort, as it always has. The wing row stays hidden until a wing has a member. A player who never makes a wing keeps every key they know.

One ship, one wing

A ship has one stance and one formation. Overlap is why the old groups misbehaved, so a ship that joins a wing leaves any other wing and takes the stance of the new one.

Follow means one thing

Keep your formation slot around the wing's anchor. Stance decides only whether you may leave the slot to fight, and the leash decides how far.

What we built

Four wings, one Main, and a leash.

A wing is a list of escorts with a stance, a formation and an anchor of its own. Escorts in no wing are Main. An order reaches the wings you picked, or every escort when you picked none.

4Wings, A to D on F1 to F4. Escorts in no wing are Main, on F5.
3 sHow long a wing stays after a focus kill with nothing left to shoot, before it returns to its old order.
1 hexThe leash: how far from its slot a Follow ship may go to join a fight.
20 sHow long wing-mates stay for a Clamped mate. A tuning row, approved for 0.5.3.

The keys

Ctrl + F1 to F4

Store the ships you picked with Shift and a click on their cards as that wing. They leave any other wing and take the wing's stance when it has one.

F1 to F4

Pick that wing. Orders, stance and formation then reach it alone. Shift adds or removes a wing. A double-tap inside 400 milliseconds shows the wing's sector.

F5

Pick Main, the ships in no wing, and return to your own sector.

Esc

Come back from a viewed sector first. Pressed again, it clears the wing pick, so orders reach every escort once more.

F A G H D R Space

The same keys as before: Follow, Attack Move, Focus Fire, Hold, Defend, Retreat and Regroup. They act on the picked wings, or on every escort when none is picked.

Its own stance and formationSet a stance or a formation on a wing and every member takes it, and so does a ship that joins later. The six formations are stack, line, wedge, box, loose and relative, and each wing has one.
Its own anchorA wing follows the ship you fly. Hold or Attack Move pins it to a sector, and its Follow ships then form up around that sector, not around you. Follow or Regroup puts the anchor back on your ship.
Its own focusA left click on a contact sends the picked wings in your sector onto it. A wing that kills its target takes the next one its stance allows, and goes back to its old order after 3 seconds with nothing to shoot.
joinsstays
The leash. A ship on Follow leaves its slot for a fight one hex away or closer. A fight two hexes from the slot is not its fight, and it deals with what is in its own sector.
HELDmates stay and fire, up to 20 saway
Clamp cover. One wing B ship is held. The other two stay in the sector and fire on the holder first, while you and wing A fly on. Retreat or Regroup lets them go.
Active ship Wing A ship at its slot Wing B ship Hostile Held by a Clamp

Schematics drawn for this post, not captures. The shapes are illustrative. The rules they show are the real ones.

As a pilot lives it

Build a flank, send it, and watch it from where you sit.

Everything below is a request. The server owns the wings, the picks, who fires and who is held. The client sends keys and draws what the snapshot says.

  1. Make a wingShift and click three fleet cards, then press Ctrl and F1. The log says the wing holds three ships, and a wing row appears on the bar.
  2. Pick itPress F1. The bar's target label reads Wing A: 3 ships. Stance and formation buttons now show that wing's state, with a mark when several picked wings differ.
  3. Give ordersPress a formation, a stance, or H to hold. Only the picked wing answers. Everyone else carries on following you.
  4. FightA left click on a contact sends the picked wings in your sector onto it. When the target dies the wing stays for the next one, then returns to its order after 3 seconds of quiet.
  5. Look awayDouble-tap the F key of a wing that is fighting elsewhere. The camera and the contact cards move to its sector. A click on a card focuses that wing. Esc or F5 brings you back.
LogWing A holds 3 ships.
LogWing A picked: 3 ships.
LogFormation line on Wing A.
LogHold: 3 ships.
LogFocus fire: 3 ships.
LogOrders go to every escort.

Wording follows the notes the server writes in fleet.py. The ship counts are examples that the server fills in.

The viewed sector

The contact cards follow one sector.

By default that sector is the one you fly in, so a fleet that fights in one place sees no change. Look at a wing that fights elsewhere and the cards list that sector's hostiles. The cards header adds the wing letter.

Your own ship keeps its own lock and fires only in its own sector. If the wing has no ships left, the view returns by itself.

A fleet of six Carriers in a small column on the left, one fighting. On the map a red hex holds the fleet. On the right, contact cards for two Breaker Patrols list their shield and hull bars. Along the bottom, the command bar shows a GROUP row with chips F1 to F4 and the label 5 escorts.
Two contact cards for one sector, from the 0.5.2 itch page refresh. This capture shows the old command bar with its GROUP chips and no wing row.
Reading a chip

One chip per wing, six facts.

  • B·3* is the wing letter and the ships in space. The star means the ship you fly is one of them.
  • G is the order letter: F, A, G, H, D, R, or a loop arrow for Regroup.
  • 64% is the wing's hull as a share of its full hull.
  • !3 is the hostiles in the wing's sector.
  • ◎ means it is firing on a chosen target.
  • HELD means a member is held by a Clamp.

A chip flashes like the danger cue when its hull drops or a member is held. With reduced flash on, it takes a steady tint instead of flashing.

B·3* G 64% !3 ◎ HELD
Schematic drawn for this post, not a capture. The text is built the way chip_text in command_bar.gd builds it.
Under the hood

A wing map, a scope rule, and a viewed sector.

The wing code lives in fleet.py, which grew from 690 lines before the wing commit to 1158 now, the logout berth included. The client side is command_bar.gd, 467 lines, and a few functions in main.gd.

Wingsfleet.py keeps data["wings"]: a slot to its members, stance, formation, anchor, cell and relative offsets, plus wing_sel. ensure_account rebuilds it from saved data and migrates old groups.
Scope_order_scope decides who an order reaches. _wing_map builds ship id to wing slot once a tick, so every lookup in the 50 millisecond step is a dictionary read.
Views_fleet_view and _wings_view put each wing's ships, hull, hostiles, focus and held count in the snapshot. The client only draws them.

One wing order, as a call chain

  1. AskAn F key sends wing_select, and an order key sends an order op. The client does not decide who answers._on_wing_key, _send
  2. ScopeThe server turns the op into ships and wing slots: named ships, the named wing, the card picks, the picked wings, else every escort._order, _order_scope
  3. PinHold or Attack Move moves the wing's anchor to a cell. Follow or Regroup moves it back to the active ship._wing_anchors
  4. SteerEach tick builds the wing map, counts each ship's index inside its wing, and asks where the ship wants to be._tick_fleet, _wing_map, _steer
  5. DecideThe answer is the slot, a fight inside the leash, the stay after a focus kill, or the stay for a held mate._desired, _cover_stays, formation_cell
  6. Fire and showThe clamper becomes the first target. The next snapshot describes each wing, and the chips redraw._pick_target, _engage, _wings_view, chip_text

The main functions

ensure_account, _wings_from_groupsfleet.py

Rebuild the wings from saved data. An old save's groups become wings, the lowest slot keeps a ship that sat in two, and the account formation seeds each wing.

_order_scopefleet.py

Who an order reaches. Orders, stances and Focus Fire all ask it. A formation uses the wing part only, because a formation belongs to a wing and not to a card pick.

_wing_set, _wing_selectfleet.py

Ctrl and F store the picks as a wing and move those ships out of any other. F picks a wing, Shift adds one, and no slot clears the pick.

_wing_anchorsfleet.py

The pin. Hold takes the first member's sector, Attack Move takes the target sector, and Follow or Regroup hands the anchor back to the active ship.

_desiredfleet.py

The one place that decides where an escort wants to be: its slot around the wing's anchor, a fight inside the leash, the stay after a focus kill, the stay for a held mate.

_mate_clamper, _cover_staysfleet.py

Find the hostile holding a wing-mate in this ship's sector and keep Follow and Defend ships there for up to 20 seconds. Passive ships do not stay, and Retreat and Regroup ignore the rule.

_broadcast_focus, _wing_focusfleet.py

A left click focuses the picked wings in your sector. A click on a card in a viewed sector sends wing_focus, which checks that the target is on the map and that one of your ships stands in its sector.

_wings_viewfleet.py

Per wing: members, ships present, hull share, hostiles in its sector, focus target, held count and anchor.

_view_wing_of, _view_contacts, _return_viewmain.gd

Move the camera and the contact cards to a wing's sector and bring them back. A snapshot with no wings field lists contacts as before.

Choices we are happy with

One scope function.The order of the fallbacks, from named ships down to every escort, is written once. Orders, stances and Focus Fire cannot drift apart.
A pin is only an anchor.Hold and Attack Move do not add a new order kind. They move one field, and the formation code already knew how to form up around a cell.
Old keys keep working.The old op names group_set and group_select still answer, and the snapshot keeps a groups mirror for one release, so an older client does not break against a newer server.
Stay rules have a way out.A held mate cannot trap a wing for ever. The stay lapses after 20 seconds, and Retreat or Regroup ends it at once.
The client side

The server owns the wings. The client asks and draws.

  • command_bar.gd builds the wing chips and the F buttons and writes each chip's text from the snapshot. The only wing state it keeps is which wing it is viewing.
  • main.gd reads F1 to F5, the double-tap window, Esc and the viewed sector. A click on a card while viewing sends wing_focus for the viewed wing.
  • The fleet card in ship_tile.gd leads its role line with the wing letter, for example B · FOLLOW · DEF. _paint_wings in main.gd draws the wing letter beside each escort tile's order badge, and one line from each picked wing to its target or its pinned sector. The viewed wing's sector gets a ring, and formation slots show only for picked wings.
  • A client that gets a snapshot with no wings field draws the bar and the cards as it did before.
What the code looks like in numbers
1158Lines in fleet.py, up from 690 before the wing commit
467Lines in command_bar.gd
23Unit tests for wings and the logout berth, 15 and 8
400 msThe double-tap window on an F key
What the tests prove

What is pinned, and what is not.

The wing tests are WingTest and the berth tests are LogoutBerthTest, both in test_alpha.py, and the commit hook runs them on every commit. The client self-test covers the bar.

Pinned

  • test_old_groups_become_wings_and_a_ship_keeps_the_lowest_slot: the migration, the formation seed, and the groups mirror in the snapshot.
  • test_orders_reach_the_named_wing_the_selected_wings_or_every_escort: the scope order, including the pick clearing so orders reach everyone.
  • test_each_wing_forms_up_in_its_own_formation: a line wing and a wedge wing stand in different hexes.
  • test_a_held_wing_is_pinned_to_its_sector_and_ignores_the_active_ship: after Hold the lead flies off and the wing stays. Regroup puts the anchor back.
  • test_after_a_kill_the_wing_stays_for_the_next_target_then_reverts: the wing takes the second hostile, then reverts after the linger.
  • test_a_fighting_ship_leaves_its_slot_only_inside_the_leash: a slot two hexes out stays put, a slot one hex out joins.
  • test_wing_mates_cover_a_clamped_mate_until_told_to_retreat and test_cover_lapses_after_the_cover_seconds: the mate stays and shoots the holder, Retreat lets it go, and the stay lapses.
  • The client self-test builds the chips from a canned snapshot and checks the text Wing B: 3 ships, the mixed-stance marker, HELD, the contact filter by viewed sector, and an old snapshot with no wings.

Not pinned, or thin

  • Only Hold has a pin test. Attack Move pinning has none.
  • The help text and our notes say a wing on Defend is pinned too. The code moves the anchor only for Hold and Attack Move, and no test covers Defend.
  • No test presses F1 to F5, the double-tap, or Esc. The self-test sets the viewed wing directly and calls the filter.
  • The live-socket checks alpha_check.py and playtest_check.py never set a wing, and load_check.py does not either. The wing pass runs inside the 50 millisecond step, and we have not measured it against the 25 millisecond target.
  • The formation tables have five slots, so a sixth ship in one wing sits on its anchor.
  • The map overlay is built but untested on screen. main.gd draws a wing letter beside the order badge on each escort tile and one line from each picked wing to its target or pinned sector. No test draws it, and we have not checked it in play.
What we learned and what is next

Small lessons from splitting a fleet.

Keep the default the same.The wing row stays hidden until a wing has a member, and an order with nothing picked reaches every escort. A unit test pins that nothing picked reaches every escort, and the client self-test checks that a snapshot with no wings field leaves the wing row hidden.
Give every stay rule a way out.Letting wing-mates cover a clamped ship was an accepted rule, but an unbreakable Clamp must not trap a wing. So the cover lapses after 20 seconds, and Retreat or Regroup ends it.
A fight in two places needs a rule.A review question asked how the contact cards cope with fights in several sectors. We answered with the viewed sector, and wrote it into the design note as its own section before building.

Where we would push next

  • Settle Defend. Either the code pins a wing on Defend as the help text says, or the help text changes.
  • Run the wing pass through load_check.py and compare it with the 25 millisecond target.
  • Watch the numbers in play. The 3 second linger, the 1 hex leash, the 20 second cover, the 15 second berth and the 20 second drop window are all tuning rows, so what testers tell us can move them without a code change.
  • Look at the map overlay in play. The wing letter on each tile and the line from each picked wing to its target are in the client, but nothing has checked them on screen yet.
One more rule in the same build

Log out, and your fleet goes home.

Until 0.5.2.1 a fleet you left in space stayed in space after you logged out. In 0.5.3 it does not.

15 sFrom a Log out click to the berth at Haven, with the fleet's cargo. A tuning row.
20 sHow long a dropped connection may stay gone before it counts as a logout. A tuning row.
At onceEvery ship in space gets Retreat the moment the logout or the lapsed drop begins.
Dock firstOn Layer 6 and in a Rift there is no Haven, so nothing berths and the ships stay in space.
  • A ship destroyed in the 15 seconds is lost, as it would be in any fight. A Clamped ship berths when the Clamp ends.
  • Logging back in cancels the berth. Your fleet keeps its Retreat orders and you fly again.
  • The client sends the logout on every Log out click. If ships are in space it asks first, and says what will happen in plain words.
  • An offline account with a berth running still ticks, and the server saves it when the berth begins, when it ends, and on shutdown.

The code is logout_begin, note_drop, _tick_drops and _tick_logout_berth in fleet.py. LogoutBerthTest has eight tests: the retreat and the berth with cargo, a ship lost in the window and a held one, the drop grace, the cancel on login, the logout op, and three that cover saving.

We are open to suggestions on how to handle a dropped connection better.

Asks3 ships in space will retreat at once and berth at Haven 15 seconds after you log out, with their cargo, as if you had docked. A ship destroyed in that time is lost, and a ship held by a Clamp berths when the Clamp ends. The same happens if your connection drops and stays gone.
AsksIt is safest to dock before you log out.
LogLogged out: the fleet retreats and berths at Haven in 15 s.
LogYou are back. The berth is cancelled; your fleet keeps its Retreat orders.

Wording follows the confirmation in main.gd and the notes in fleet.py. The client fills in the ship count and the seconds.

Join the open alpha

Come and split a fleet with us.

Keel is free to play on PC, and wings reach the open alpha with 0.5.3. Find us in the Keel Discord, tell us what wings 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. When 0.5.3 is out, hold a choke with one wing, send another after the stack next door, and tell us what the contact cards did while you did it. Dock before you log out.