Roll-up patch · v0.31.15 → v0.31.29 · 30 April 2026
Steam connected. Combat audited. Army upgraded force-wide.
Commanders,
Fourteen patches since BREAKTHROUGH shipped. The headline arc this time isn't one big system going live — it's Steam integration finally working end-to-end, plus a class of long-standing combat and progression bugs getting cleaned out at the root. If you were one of the early-access players who registered before Steam launch wired up — your account now links cleanly the first time you sign in. If you ever bought a Tech Tree upgrade and noticed your existing tanks didn't seem any stronger — that's fixed at the source. If an enemy helicopter ever shot your Razvedchik on the way down despite your AA reaction killing it — also fixed.
This is a "everything you reported actually got addressed" build.
The biggest thread in this rollup. v0.31.18 through v0.31.25 is one continuous arc of fixing Steam launch, then making account migration self-serve.
The Steam-launched build was failing with cryptic 502s on every cold start. Diagnosed in three layered patches:
v0.31.19 — F12 devtools enabled in release builds. Tauri 2 strips devtools by default; F12 was a no-op for every player who hit a Steam-launch issue. Now F12 opens a real WebView inspector in shipped builds.
v0.31.20 — Per-step diagnostic logging through the entire Steam auto-login chain. getAuthTicket previously swallowed all errors with catch (_) { return null; }; now every step prints what it did, what it returned, and why.
v0.31.21 — Wrong Valve API host. The actual root cause: Steam ticket validation was hitting the wrong API
Pre-Steam HexArmy users who launched via Steam were getting auto-routed to a numbered throwaway account because their original callsign was already taken.
A blue "Steam detected: " banner explains what's happening so the linkage isn't a surprise. Idempotent if already linked. Refuses to overwrite a different existing Steam linkage with a 409.
Once you're signed in, the home callsign panel now shows your Steam linkage status directly under the XP bar. Three states:
Linked — green pill: "STEAM LINKED as "
Unlinked + Steam running — "Link to Steam" button: one click links your Steam profile to your account, no password re-prompt (the session already proved account ownership; the Steam ticket proves Steam ownership).
Unlinked + no Steam — small grey "Steam Not Detected — Launch via Steam to Link" hint.
The button uses a new authenticated endpoint POST /api/auth/steam-link-current that does the same merge logic as /steam-link but uses the current session's user_id as the keep account. Same self-healing throwaway behavior.
Until v0.31.24, manually-registered HexArmy accounts started with profile_public = FALSE — so newly-signed-up players were invisible in the Personnel Dossiers tab and couldn't share their Service Record in multiplayer lobbies until they hunted down the toggle in account settings. Steam-linked accounts always defaulted to public. This release brings everyone in line:
New accounts: registration INSERT now sets profile_public = TRUE explicitly.
Existing accounts: a one-shot migration (scripts/flip_profile_public.js) flips every existing FALSE/NULL row to TRUE in a single transaction.
The opt-out toggle still works for anyone who actually wants privacy.
User-reported, with a screenshot of the comms log: an AI Apache moved into the player's airspace, the player's Pantsir reaction-fired for -17 damage (lethal), and the very next log line showed the dead Apache scoring a hit on the player's Razvedchik for 1 damage with the FRONT modifier.
Root cause: doAttack() and attackCity() checked attacker.attacked and attacker.movRemaining before refusing to resolve combat, but never checked attacker.hp > 0. The AI move-then-attack flow runs checkAAReaction() (which can lethally damage the moving air unit) and then unconditionally falls through to the attack call.
Fix: one-line if (attacker.hp <= 0) return; at the top of both combat functions. Defense in depth — every caller (current and future, AI and player and multiplayer-sync) is protected from the same class of bug.
User-reported: NK Koksan units were moving 1 hex slower than their base MOV stat indicates, despite the doctrine description reading "Non-Inf/Art -1 MOV" and the modifier being literally named nonInfantryArtilleryMovPenalty — both of which explicitly exempt artillery.
Root cause: a stray application of the modifier inside the artillery-specific branch of applyFactionModifiersToUnit, hitting artillery for -1 MOV against intent. Likely an accidental copy-paste from the structurally-similar artilleryMovPenalty line. Removed the misplaced application; the correctly-gated application below stays.
Two long-standing engine bugs found during a flanking-mechanics audit:
Suppression dead code. The unit.suppressed flag was being read in the unit info panel and combat resolution, but never written. Artillery direct hits and AoE barrages now correctly set suppressed = TRUE on surviving targets, and endTurnCleanup applies the documented -1 MOV next turn.
Advance onto full-HP neutral structures. The capture branch in attack resolution required hp <= 0, which excluded full-HP neutrals an attacker had just walked onto. Broadened the capture condition.
The Field Manual flanking section was also rewritten to make the HQ-anchored facing rule explicit: a defender's flank line runs from their position to their own HQ — attack from the side or rear of that line, regardless of how many other units are pinning the target.
User-reported, and a real surprise once we found it: paying RP for an upgrade like Armor L1 (+1 ATK on Tanks) only affected newly-produced tanks. Every tank you'd already fielded kept its old ATK value.
The research effects baked stat deltas into units only at spawn time via applyFactionModifiersToUnit. Effects persisted on the unit object, never re-evaluated. So mid-game purchases of Infantry L1 (+1 ATK Infantry), Infantry L2 (+2 HP Infantry), Air L1 (+1 MOV jet/heli), Naval L1 (+3 HP destroyer/aa_ship), etc. — all silently skipped your fielded forces.
purchaseResearch() now iterates every existing player unit of the matching type and applies the delta in place. The MOV special-case bumps movRemaining for the current turn so a +1 MOV upgrade kicks in immediately rather than waiting for endTurnCleanup. Comms log reports how many units received the boost: "Research: Armor Doctrine L1 — Field Drill: +1 ATK on Tanks — applied to 4 existing units".
Combat-time effects (ACC vs adjacent enemies, DEF when adjacent to friendly Infantry, the Air L6 Stealth Bomber capstone) were already applied dynamically and are unchanged.
Spawn-time application also stays — future units still bake research deltas in at spawn. The two paths now produce identical final state.
User-reported: the Terrain Density slider's Low/Medium/High setting did nothing on Medium-or-larger maps when any non-Standard map type was picked. Because themed map types (Continents, Archipelago, Pangea, Highlands, Forest Basin, River Networks) only become selectable on maps 400+ tiles, the slider appeared to silently break on every map size above Small.
Root cause: only the Standard generator honored gameState.terrainDensity. The six themed generators all hardcoded their forest cluster counts, mountain peak counts, and lake counts — never reading the slider. Single-line fix wouldn't cover it because each themed generator has its own structure.
Fix: a shared _terrainDensityMul() helper now applies Low (×0.4), Medium (×1.0), or High (×2.0) scaling to forest, mountain, and aesthetic-water counts in every themed generator. Theme-defining structural elements stay fixed — Continent oceans (45% water), Archipelago seas (60%), Pangean coastline, Highland mountain ranges, the river network in River Networks. Otherwise a Continents map at Low density would have only 18% water and look like a holey Pangea, breaking the whole point of picking a theme.
Desert Wastes is unchanged (no terrain features by design).
multiplayer_games_id_seq had drifted behind MAX(id) — same class of bug we hit on users_id_seq previously. New maintenance script scripts/fix_all_id_sequences.js discovers every SERIAL id column in the public schema, checks each sequence's last_value against its table's MAX(id), and resets any that have drifted in a single transaction. Idempotent. Catches all future drift in one pass.
The flanking explainer in both the in-game Field Manual and tutorial.html was rewritten to use the HQ-anchored facing language. The cheat-sheet line "Pin and flank" is now "Flank to the rear" — same idea, accurate to the actual mechanic.
If you registered for HexArmy before Steam launch wired up — log out, sign back in via Steam, and the Steam linkage now Just Works. If you've been buying Tech Tree research mid-game, your existing army actually benefits now. If you've been losing units to dead-but-still-attacking helicopters, that's fixed at the root for every code path that ever does combat. If you've been ignoring Terrain Density on themed maps because it didn't seem to do anything — try it again.
Update via Steam → Properties → Betas to grab v0.31.29 today. Promotion to default branch follows after a few days of testing on beta.
The full release notes are live at
https://hexarmy.com/release-notes.html
Link your Steam. Research a doctrine. Watch every tank you own get the upgrade. ⬢