Measured before changing anything. The ticket's two halves needed opposite
fixes, and the first one made the second worse.
TOO COMPLEX. Variety was gated on fleet size alone, so a player who had
grown early met two kinds of trouble in the first wave they ever saw - a
fleet of four opened on two lancers and three chasers. Each roster entry now
carries a min_wave as well, so the openings teach one enemy at a time
whatever the fleet already looks like.
That alone pushed wave 1 against a fleet of six from a mixed six to SEVEN
chasers: budget that used to buy a lancer now bought two more chasers. So an
early wave also has a size ceiling, 2 + wave_number * 2. Wave 1 is four
chasers at any fleet size; the cap stops binding by wave 6 and nothing about
later balance moves.
before wave 1 fleet 4 -> 2 lancers, 3 chasers
wave 1 fleet 6 -> 1 sniper, 2 lancers, 3 chasers
after wave 1 fleet 4 -> 4 chasers
wave 1 fleet 6 -> 4 chasers
ARRIVE AT THE SAME TIME was literal: the whole wave spawned inside one frame
as a closed ring around the fleet, which reads as a wall rather than as an
attack. The first hull still lands immediately so a wave is never felt as
late, and the rest follow every 0.45s. Same enemies, same bearing, same
total - they simply do not all exist at once.
Four existing cases objected, and each was right to: they encode intents
that still hold LATER - a large wave must be varied, fleet growth must open
more kinds, a wave can field a lancer at all, and killing a wave must bring
the quiet back. The first three now ask at a wave number where variety is
permitted rather than being weakened. The fourth runs the arrivals out
before killing anything, and gained an assertion that doing so ADDS enemies
- which fails if the staggering ever collapses back into one frame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#229 - a volley got louder for every ship in it.
Every hull owns its gun and asks for its own shot sound, so eight ships
firing on the same cooldown started eight voices playing the SAME sample in
the same frame. Those amplitudes add, so a full fleet was the loudest thing
in the game by a wide margin.
play() now takes a per-sound coalescing window. A volley is one event to the
ear, so it gets one voice. 70ms, which the test checks is inside the fastest
gun's 500ms cooldown - it collapses "six guns at once", never "six shots in
a row". PICKUP and ASTEROID_BREAK get one too: ore arrives in bursts when a
rock breaks and that is one pickup to the ear. Deaths and the boss scream
are left alone deliberately - several things dying together IS several
events.
test_sfx's pool-saturation case was driven by shots, which can no longer
saturate anything by design. It is driven by deaths now, which is what the
pool actually has to absorb, and it gained a case asserting a volley takes
one voice - measured against the real pool rather than the gate.
#234 - gas was not reliably in reach of the opening.
Gas giants are gated behind two independent noise fields, the SPACE biome
AND the planet roll, so whether any sat near the spawn was luck. Measured,
this seed lost: ZERO gas planets within four chunks, the nearest five away
past five rocky ones. Gas is an early need - the burner, ammunition - so the
opening cannot depend on the noise being kind.
SPAWN_GAS_GIANT_CHUNK at (2, -2), authored like OCTOPUS_BIOME_CENTER rather
than found by searching outward: a fixed chunk is reproducible from anywhere
without walking the map, which is what the rest of WorldGen is built on.
The override is applied AFTER every rng draw, for the same reason the dev
planting is - forcing one chunk must not shift what any other chunk rolls.
Confirmed by probe: the five other planets within four chunks are identical
before and after, and the distant gas giants at (-5,-4) and (0,-5) are still
where they were.
Both verified in reverse: without the window, "8 ships firing together
started 8 shot sounds"; without the chunk, "no gas planet within 4 chunks of
the spawn".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
Godot writes one per script and the project tracks the other 412; these
were left behind when their tests were added. Nothing but bookkeeping.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR2Mdk42ghEpeyXPe4UFyn
Clicking the fader did nothing on a desktop: it fired the guns instead.
Unhandled input reaches the last nodes in the tree first, and the fader's
mouse handling lived in GameController - the script on Main, the ROOT, so
it was served last of all. MouseCombatController is a child of it and marks
every left press handled to fire, so the press never arrived.
The fader asks for its own mouse events now, from the UI layer, which sits
after the combat controllers: the control the pointer is actually on gets
the press ahead of the guns behind it. The handlers themselves are the same
wrappers #118 wrote.
The test for #118 could not see any of this - it handed the event straight
to Main._unhandled_input, which is precisely the delivery the real game
never makes, so it passed against the bug it was written for. It pushes
through the viewport now, with mouse aiming on, and checks the guns stay
quiet. Confirmed it fails without the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR2Mdk42ghEpeyXPe4UFyn
max_fleet_size() clamped itself to DEFAULT_FORMATION.size(), so the ceiling
on the fleet was a property of the LAYOUT - the wedge ran out of authored
positions at twenty - rather than a rule anyone decided on. Capacity is the
supplied Fleet Offices and nothing else now.
Dropping the clamp alone would index off the end of the formation array, so
the wedge continues by rule instead of stopping: formation_slot() grows it
on demand, each further pair stepping 30 further out and 40 further back,
which is what the authored rows already do - the first twenty slots come
back unchanged. A grown slot is rotated by formation_rotation so a fleet
that outgrows the array after the player has pivoted does not sprout hulls
facing the original heading, and that angle is saved with the layout.
Two "fleet is full" messages pointed at the vanished ceiling. Both now say
what actually buys a berth, and the hangar's stops claiming it is research,
which it has not been since #219.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR2Mdk42ghEpeyXPe4UFyn
The layout is authored in CANVAS units. With stretch "canvas_items" and
aspect "expand" over a 1920x1080 base, a 720x1280 phone scales by 0.375 and
gets 1920x3413 canvas units - while ANY 16:9-or-wider screen lands on
1920x1080 units however many pixels it really has. The canvas is three times
shorter, so a 148-unit button went from 4.3% of the screen's height to
13.7%. Measured at 3.16x, and confirmed twice: once by hand off the scene
file, once by the test failing with the rule disabled.
Nothing collided. That was checked pair by pair on both canvases and the
same six pairs overlap on each, all of them same-slot alternatives. The
fault was only that everything was enormous.
HudScale captures each HUD control's authored offsets once, then moves it
toward the edge it hangs off and scales the NODE. Scaling the rect alone was
tried first and was wrong: half the HUD draws its own contents at fixed
sizes - ResourceReadout lays its rows out in code, Labels carry their own
font size - so shrunken boxes kept full-size content and the build button
slid up underneath the resource list. Scaling the node takes content and
children with it.
The cost is that position (parent space) and size (local space) now differ
by that scale, and a lot of code here assumed they did not. Five places:
ResourceReadout.content_bottom() added a scaled position to an unscaled
height, overstating itself by more than a button - so _seat_hud_columns
pushed the whole column down to clear space nothing occupied. A live bug,
not just a bad measurement.
HUD_COLUMN_MARGIN would have been the one gap still phone-sized.
_layout_context_buttons wrote its stride and pitch unscaled, landing the
action row on the building-action row - which is the second of the two
regressions test_hud_layout was written for, in a new disguise.
test_hud_layout and test_hangar_management compared authored rects.
And HudScale reapplies on resize, which wiped the two layouts BuildController
writes at runtime - so dragging a desktop window left the context row where
it was. Both re-run on size_changed now, deferred, because which of two
siblings connected first is not a thing to depend on.
test_hud_layout ran at one window shape, which happened to be the desktop
canvas. It now runs every check at both, so this cannot hide at whichever
shape was not being exercised.
Rejected: scaling the UI CanvasLayer does not work at all - Control anchors
resolve against the viewport rect, so bottom-anchored elements land
mid-screen. A single scaled wrapper Control with the HUD reparented under it
is the genuinely clean shape and would need no scale-aware arithmetic
anywhere, but it changes every node path and would break every get_node in
the controllers and tests.
MIN_SCALE floors the shrink at 0.45 rather than the proportional 0.316: a
landscape phone gets the same short canvas as a desktop, and there the
sticks are still thumb targets. That leaves the HUD at 1.42x its phone share
on a PC rather than 1.0x.
177/177 at JOBS=3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
Loading a save fell through to the brand-new-game branch in
PlanetAtmosphereController, which spawns STARTING_SHIP_COUNT interceptors at
the world origin. So resuming stranded the player in empty space with the
wrong ships while their base sat wherever they had left it - thousands of
units away on a charted map.
fleet_position was in the save all along, but nothing ever read it: only
Fleet writes it, as a mirror for payment-range checks.
A resumed run now spawns at get_mothership_position(). That is already the
canonical home - BuildController._check_fleet_wipe rebuilds a wiped fleet
there - so this is the existing answer to "where does the fleet belong",
not a new one. It falls back to the position the save was written at, and
only then to the origin, because a run can have no mothership standing.
resumed_from_save is a one-shot handoff like has_return_position, and is
deliberately NOT saved: it describes this launch, not the run.
Verified by reverting the branch: "resumed at (0.0, -48.0), but the
mothership is at (7400.0, -3100.0)".
One note for whoever writes the next fleet test: asserting a three-ship crew
proves nothing without capacity. Since #219 the fleet holds BASE_FLEET_SIZE
(1) plus one berth per supplied fleet office, so spawn_ships clamps to 1 and
the assertion passes against the wrong number. Case A gives the save two
offices and checks max_fleet_size() first.
176/176 at JOBS=3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
All 405 other .gd.uid files are committed; SaveGame.gd's was left behind in
fb35871. Godot regenerates a missing one with a fresh value, so an untracked
uid means every checkout gets a different id for the same script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
The append-only rule was stated at the top of Icon and then restated eight
more times down the list - "appended, same rule", "inserting anywhere
earlier renumbers every icon int already stored in every scene" - about
twenty-five lines carrying one fact. What each comment actually contributed
was the glyph's subject: a wide hauler hull, a directional sensor dish, the
workshop and the drone it sends out.
Rule once at the top, then each entry says what it depicts. The DISTORDER
note stays, reworded: the name is stale (it became the Energy Propulser) and
CANNOT follow the art without renumbering, which is worth knowing.
The enum order is byte-identical - 48 entries, verified against HEAD before
committing. Reordering it would silently repoint every icon in every scene,
which is the exact hazard the comment is about.
This is the whole of what the sweep found. Searched for the same failure
elsewhere with a near-duplicate detector and no other file repeats itself;
the detector fires on the pre-fix version of this file, so its silence is
worth something.
The ticket's broader premise did not hold. Across 181 non-test files (24946
lines, 21% comments): the 79 single-line comments sitting above code state
intent rather than action; GameState has no function comment that restates
its own name and exactly two single-line in-body comments in 4219 lines; and
the densest files are dense because the reasoning is - SurfaceBackground
gives the arithmetic behind its bake resolution.
Still open, and deliberately not done alone: 491 issue-number citations.
Stripping them is a real simplification but they are traceable history, and
the ticket warns against deleting what cannot be re-explained.
173/175 in the run, both misses being 120s timeouts under JOBS=4 contention;
they pass in 81s and 79s run uncontended.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
JSON at user://savegame.json rather than a binary resource: a save that can
be opened in a text editor is diagnosable from a bug report, and
ResourceSaver would tie the file's shape to GDScript class layout, which is
the thing that keeps changing.
The run is not plain data, which is the actual work here. A tagged codec
carries Vector2, Vector2i, PackedStringArray and the Inventory objects
nested inside `buildings`. explored_chunks is keyed by Vector2i and JSON has
no such key, so every key carries its own type prefix ("v2i:3,-4") - without
that a planet id could come back as a chunk coordinate.
WHAT is saved is an explicit list of 30 fields, not a dump of GameState's
58 vars. A dump would carry transient junk - which pointer last touched the
screen, whether a dialog holds the camera - and would break silently on
every rename. The cost is that new progression must be added to the list;
the benefit is that forgetting is a missing feature, not a corrupt save.
Versioning, as the ticket asked. MIGRATIONS walks a save up one version at a
time; MIN_SUPPORTED_VERSION is how a break too messy to migrate gets
DECLARED rather than bodged. A save that cannot be walked forward is refused
with a player-readable reason, because a save that loads subtly WRONG is
worse than one that refuses and says so. Adding a field needs no bump: it
arrives missing from old saves and keeps its new-game value (case F).
The menu offers Continue only when there is something to offer. A save this
build CANNOT open is still shown, disabled, with the reason underneath -
silently hiding someone's run reads as lost rather than incompatible.
Autosave every 30s, plus on close and on Android's app-pause, since the
phone kills backgrounded apps with no warning. Skipped during the prologue.
That autosave writes to the same user:// path every test that loads
Main.tscn shares, so it could make Continue appear and shift the menu stack
under test_main_menu depending on what ran first. That test clears the save
before building the menu.
Verified by breaking it: neutering the key decoder fails with "chunk keys
came back as [v2i:-11,0, v2i:3,-4]".
Still absent, deliberately: no save-on-demand button, and no route back to
the menu from in-game, so a resume currently needs a relaunch.
175/175.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
BuildingField.place() dropped a structure at the fleet centroid with no
overlap check of any kind, so they could be stacked.
It reads worse than it looks. BuildingField decides which structure the
fleet is standing on by testing the centroid against interact_radius and
returning the FIRST match, so two structures inside one another make that a
question of list order rather than of where the player flew - you could fly
to your workshop and open the crafter.
The separation is interact_radius itself (150) rather than a new number,
because that is exactly the circle the ambiguity comes from. Buildings draw
to a radius of roughly 60, so this leaves a visible gap rather than letting
them touch.
Gated in three places: the info popup (the button greys out and says which
structure is in the way), _confirm_build, and _try_place - the preview goes
stale as the fleet keeps moving.
Ordering matters in one case. A unique structure replaces itself by
demolishing the old one BEFORE placing the new one, so a placement refused
afterwards would leave the player with neither. The check runs before the
demolition and excludes the structure being replaced. Case D pins it.
Two existing tests encoded the old permissive behaviour and now fly clear
first; neither is about placement:
test_build_info_popup builds every menu option from one position, so
each structure blocked the next
test_payments_use_fleet_hold builds a mothership on the ship lab that
an earlier case injects at the origin
Also fixes a bug from 37e2e89: Sfx.warm() called add_child() from _ready(),
which Godot refuses outright - it printed an error and never pre-built the
pool, defeating the point of warming it. Deferred, like Music.ensure().
173/173.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
Reported from the game: sounds sometimes start a bit late. The samples were
the suspect, so they were measured first - and they are clean. True digital
silence at the head of each file:
seven of nine start on sample 1
sfx_gas_blast 0.2ms
sfx_black_hole 1.5ms, then a deliberate swell
The black hole's slow attack is the sound it was written to be - make_sfx.py
says it "swells across most of its length instead of decaying from the first
sample" - so there is nothing to trim without breaking it.
The delay was in the playback path, and "sometimes" was the clue:
play() called load() every time. A cache hit after the first, but the
FIRST play of each sound was a synchronous read and decode on the frame
the sound was asked for. Ten sounds, ten occasional late starts.
_pool() built its twelve AudioStreamPlayer nodes lazily, on the first
play() call - one bigger stall, every session.
The catalog is preloaded now, so play() never touches the disk, and
Sfx.warm() builds the voices next to Music.ensure() at startup.
Case F pins the thing that was originally suspected, since make_sfx.py
regenerates these and a lazy envelope would hide the delay somewhere nobody
would look. It reads the source .wav's RIFF chunks rather than the imported
stream: these import as QOA (compress/mode=2), so AudioStreamWAV.data is not
PCM. The first version of that check was vacuous for exactly that reason and
passed at a 0.5ms threshold; it now reports "9 samples checked, worst attack
sfx_black_hole.wav at 1.5ms" and fails when tightened.
Not touched: project.godot has no [audio] section, so output_latency is at
its default. On Android that buffer is usually the dominant latency, but
lowering it risks crackle on real hardware - worth doing with someone
listening, not blind.
172/172.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
Planet.gd calls visual.configure() BEFORE add_child(), and the SceneTree
defers _ready() to the first frame - so configure() built the five rings,
and _ready() then threw them away and built five more. Eighty antialiased
polylines tessellated per black hole instead of forty, plus five nodes
queued for free.
Confirmed by instrumenting _build():
_build() called, existing children=0 <- configure()
_build() called, existing children=5 <- _ready()
_ready() now uses the same is-it-built guard configure() already had.
Note for anyone chasing the same thing: this is NOT the disc's steady-state
cost. Measured, the geometry rasterises zero times over sixty frames (the
NeonPiece refactor works), and rendering the disc costs 0.1 ms against a
133 ms softpipe floor. The disc is cheap once it exists.
172/172.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
The "no research data" explainer is the only dialog in the game that opens
ON TOP of another one - the skill tree or the ship lab, from the button that
was just refused. IconButton._blocked_by_modal() refuses input to any button
sitting behind an open panel, and it had no notion of stacking order: it saw
the lab still visible, saw it was not an ancestor of the popup's close
button, and killed the touch. Every button in the explainer was dead,
including the only way out of it. Reported from the game.
The check now compares draw order. A panel that this button's own panel is
drawn on top of is behind it, not over it, and is ignored; a panel drawn
above it still blocks, so the lab's own buttons stay dead while the popup
covers them. Buttons belonging to no panel - the HUD - are unchanged and
still blocked by anything open.
The existing test missed this because it dismissed the popup by calling
close_menu() directly, which is not what a player can do. Case D presses the
button, and asserts the reverse too. Verified in both directions: with the
fix reverted it fails with "the explainer close button is blocked by the lab
behind it - the dialog is inescapable".
172/172.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
Nothing under ui/ or vfx/ was marked @tool, so every hand-drawn control
rendered as an empty rect in the editor. BuildMenu.tscn positions its
buttons with hand-tuned pixel offsets (offset_left = -204) around a glyph
identified only as `icon = 8` - laying out invisible boxes by number.
@tool on the three scene-placed components that are safe to run in the
editor, covering 23 of the 34 placements: IconButton (20 scenes),
TouchJoystick (2), NeonPanel (1).
Two of them mutate persisted properties on _ready, which under @tool
would write computed values back into the .tscn every time the scene was
opened, so both are guarded with Engine.is_editor_hint():
NeonPanel._fit_to_screen() rewrites all four offsets
TouchJoystick._relayout() overwrites max_radius, which is @export
IconButton's _gui_input is guarded too, so clicking a button on the
editor canvas does not emit `pressed`.
The autoload-heavy drawers (ResourceBar, PriceTag, QuestPanel,
ResourceTransferList, SkillTreeView, FormationEditor - 6 to 16
GameState./Quests. references each) are left alone; they need their call
sites guarded first.
Verified: 172/172 headless, and a --headless --editor pass loads the
project with no script errors. That pass only proves the scripts parse
under @tool - it never opens a scene, so it does not exercise the
in-editor _draw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
Reviewed against the rule issues #6 and #10 set for the buildings: geometry
is rasterised once and thereafter only moved, scaled or faded, because every
NeonDraw call expands into four antialiased polylines.
The planet visuals never got that refactor. The black hole rebuilt ten
glowing paths of twenty-five points every frame - forty polylines - to spin
five rings; each ring is a NeonPiece that rotates now. The research lab spent
nine draw calls a frame, eight of them fixed geometry, to turn one dish and
blink six lights; the dish is a child that rotates and the lights a child
whose modulate breathes. The gas giant's bands genuinely deform, so they
still draw, but its constant rim moved out to a child. NeonPiece gained
add_path, which it needed for open arcs.
And I had broken the rule myself. ScannerGuidance re-queued a redraw of its
arrow every frame for a colour that changes only with the destination - and
it runs with PROCESS_MODE_ALWAYS, so it was re-tessellating through every
dialog that stops the rest of the game. Guarded, and pinned by a test that
counts CanvasItem's own draw signal: 36 rasterisations in 0.6s before, one
after.
Two smaller things from the same pass: the station menu drew its condition
bar by hand where the ship lab already uses a ProgressBar node, so it uses
one now and its _draw is gone; and station/upkeep refreshes were wired into
_refresh_garrison(), which nothing but a test calls, so both menus showed
whatever was true when they opened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
_export_probe.gd is the check that found the empty planets: run inside a
built pack with --main-pack, it reports how many planet setups the registry
actually finds and what garrisons they carry. The suite runs from source and
structurally cannot see an export-only fault, so this is the thing to run
before a deploy.
The .uid files go with it because the repo tracks one beside every other
script; the ones added this session were left behind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
The registry scanned res://PlanetSetups for names ending ".tres". An export
does not ship those: text resources are converted to binary and the pack
carries "<name>.tres.remap", which ResourceLoader follows when asked for the
original path. So the scan found nothing in the web build, every planet drew
no setup, and since #211 the setup IS the garrison - every world in the
deployed game was deserted, with nothing logged to say why. Confirmed by
reading index.pck, which holds rocky_default.tres.remap and no .tres.
The suite runs from source, where the bug cannot occur, so the name
resolution is its own function and tested against the exact strings the pack
contains.
Also: the planning centre listed transporters and repair drones but
set_vessel_target() still refused anything not fleet-eligible, so both rows
had buttons that did nothing - the worst shape a refusal can take. Listing
and setting read the same rule now, and a standing order is only offered to
a crafter that can actually take it.
And two quests close the chain: the tentacle world, which the scanner will
not offer until the story hands it over, and the Warden itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
The repo tracks a .import beside every other .wav; the four #208/#210/#213/
#214 sounds were committed without theirs, so a fresh checkout would import
them from scratch and a web export would carry different resource ids than
the tree it was built from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
The scanner quest moves ahead of the gas burner and gains the hunt it exists
for: a research lab world, which pays several times what an ordinary planet
does and is the only reliable source of research data. Finding one is what
the scanner is FOR, so the conquest record now remembers which kind of world
was taken and a quest can ask for that kind.
Bombers get a pair of quests after the transporters. That pulled the ship lab
up with them - a vessel is unlocked AT the lab, so asking for a bomber before
one exists would have been unfinishable, the same trap second_hull fell into.
And both research screens now explain a shortfall instead of hiding it. The
skill tree refused a doctrine in silence; the ship lab said "already queued
or complete", which is not why it refused and sends the player to look at the
wrong thing. Both open an explainer naming what was wanted, what is held, and
where data actually comes from.
#192's rule - a tool is taught immediately before the quest that needs it -
now points at the scanner's FIRST use rather than its only one, with the
world survey still required to follow it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#216 and #219 turned two things into player work without saying so anywhere:
transporters are built and assigned by hand, and a berth in the fleet is a
structure that has to be supplied. Five quests now teach both.
For the hub: build a transporter at a crafter, then assign it. Two steps,
because they happen in different places and one quest covering both strands
whoever misses the second in front of a hub that does nothing. The closing
line warns that they wear out, since the alternative is finding out when a
route quietly stops.
For the fleet: research Berth Authority, build an office, PUT ORE IN IT, and
only then crew the berth. The stocking step comes before the vessel on
purpose - the other way to learn it is to watch a vessel you paid for be
destroyed.
And a campaign-breaking bug of my own: #219 moved capacity onto the office,
so "fly two vessels at once" - sixth in the chain, six quests before
anything that could hold a second - could not be finished at all. It sits
after the office now, and a new rule in the chain test refuses any quest
that asks for more hulls than the player can yet hold. Verified by putting
the bug back: it fails, naming both steps.
Also: the conquest quest still said "take every capture point", which #211
deleted from the game.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
A ship crafter shows what can actually be ordered there: a hull whose
research is done, and - for the support craft - one with somewhere to be
assigned. A transporter with no hub and a drone with no workshop have
nowhere to go and would sit in the hangar as spent resources, so the
structure that uses one is part of its availability. Hidden rather than
greyed, as the build menu already treats an unresearched structure, and
gated in queue_order() too so the model refuses what the menu declines to
show.
The craft buttons bound their handlers to a definition captured at startup.
With a list that changes length that is a live bug - a slot would order
whatever hull was there when the menu was built - so they bind to the slot
and resolve through the current offer.
The hub and workshop crew menus now NAME what is left in each vessel:
"TRANSPORTER 2 9 / 20 RUNS", worst first, with one wide bar for the vessel
closest to spent. The per-vessel bars they replaced drew about seven pixels
tall on a phone and carried no figure, so the number the player is deciding
on could not be read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#223: an office declared a flat buffer - 60 ore, 30 gas, 30 energy - whatever
was in its berth, so one keeping an interceptor had transporters haul it gas
and energy it can never spend, and the figure bore no relation to what it was
actually charged. Since #220 that charge depends on the berth's occupant and
on how far down the fleet the berth sits, neither of which a definition can
state.
It asks for what its own berth costs, four charges deep: enough that the tax
is payable whenever the bank has anything - a transporter round trip fits
inside four intervals - and no more, so an office is not a hoard. Same idiom
outposts and workshops already use for world-dependent demand.
A small floor stays in the definition on purpose: an empty berth asks for
nothing dynamic, so without it a newly crewed berth would start from zero and
could starve before its first delivery landed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#221: the planning centre listed only fleet-eligible hulls, so the two
vessels that most need replacing were the two it could not plan - #216 made
transporters and repair drones wear out, which is what a standing order is
for. It offers everything a crafter can build now.
That needed the census to see them: it counted the fleet, the hangar,
garrisons and transit, but not vessels standing at a hub or a workshop. An
order for a transporter would never have seen the ones already built, and
the crafters would have gone on making more forever.
#222: the ship lab's detail block restated the selected upgrade's research
point cost, which its own card shows a few pixels higher. Removed, with the
parameter and the call-site lookup that fed it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#220: every berth past the first paid one costs 1.12x the one before it, on
top of the flat per-vessel charge. A linear bill makes the tenth hull no more
of a decision than the second; compounding weighs each addition against the
whole fleet behind it. Shallow on purpose - the thirteenth berth costs about
three and a half times the second, the twentieth about seven and a half.
Fleet Offices gain a menu naming the whole bill per minute, a line per paying
berth with its multiplier, and the arithmetic in words. It renders
fleet_upkeep_breakdown(), which is the calculation the charging loop itself
runs: a readout that summed independently could quote a bill the game does
not take, which is the thing worth preventing here. The test pins that what
the menu quotes for a berth is what that office is charged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
Fleet Offices sit behind a new Berth Authority doctrine. It costs ore and no
research points, deliberately: points come only from conquering a planet,
conquering needs a fleet, and the fleet caps at its opening hull until an
office exists - so pricing the fleet's own gate in them would lock a player
behind a fight that one vessel has to win first. The research unlocks the
STRUCTURE; capacity is still the built, supplied thing #219 made it.
The tree gains a sixth lane for it, so the branch that fleet size vacated is
occupied again rather than reading as a hole.
Also fixes a shape #216 exposed: a stationed transporter drew a hull written
into its own script while a delivered one drew the definition's, and the
transporter's definition declared none at all - so one in flight was an
empty polygon. Both vessels now draw the definition, and a test pins that
the two paths agree. The repair drone had the same fault, off by two units.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#219 deleted the eight-node doctrine lane, which left the ship-lab branch
zigzagging between two lanes with nothing beside it - the same fault #70
fixed for squadron, where a chain that changed lane at depth collided with
somebody else's node. The tree runs as five straight lanes again: supply,
industry, spine, ground, vessels. No two nodes share a lane at one depth,
and every only-child stays in its parent's.
Fleet size is deliberately absent, which is why the widest lane is empty:
#219 moved it out of research and onto the Fleet Office. Said in the code so
the gap reads as a decision rather than as something dropped.
Also corrects wording #216 had made untrue: hubs no longer come with their
own transporters, and the two capacity doctrines buy a BERTH, not the vessel
to fill it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#216: transport hubs and spacial workshops no longer conjure their crews.
Transporters and repair drones are ordered at a ship crafter, assigned per
building through a new Station menu, flown out from the mothership hangar,
and spent by the work they do - 20 runs, 12 repairs, counted where the run
ENDS so nothing retires with cargo in the air or a building half repaired.
A spent vessel is gone, so the structure is short again and the player has
to build another. The menu shows what each one has left, worst first.
Craftability needed its own flag: "the crafter can make it" and "it can take
a berth in the fleet" stopped being the same question.
#217: transporters crawl at 120 rather than 150, and a Burn Drive lets them
carry energy and spend it to travel far faster. Fetching it is a leg of its
own, to the nearest store that has any - so a refuelling transporter cannot
be dispatched mid-errand - and the leg flown to COLLECT fuel is not charged
for it. Unresearched, nothing detours and nothing changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#219: fleet capacity is a built, supplied thing rather than a researched
one. The eight fleet-size doctrines are gone; capacity is one berth free
plus one per Fleet Office standing. An office charges every 25s for the
vessel in its berth, priced off that vessel's own build cost so an expensive
hull is expensive to keep, and drawn from its own store the way a gas burner
draws gas. An office that cannot pay goes dark, its vessel is scuttled, and
the player is told through the same queue quest lines use - which reaches
them on a planet surface too.
An empty berth is idle rather than starved: an office is only failing while
there is something in it to fail, and holding it disabled would leave a
berth that could never be crewed and so could never be paid for again.
Ships have no hit points and a shield absorbs a hit wholesale, so the lost
vessel is scuttled rather than damaged - otherwise a shielded hull would
survive its own eviction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#211 conquest is the fight now: a planet registers the garrison its setup
authors, each defender's own death counts against it, and the last one takes
the world. The capture points - stand in a ring, wait out a timer, once per
ring - are gone, along with the editor palette that placed them.
#212 conquest opens a dialog on the surface naming the research points
recovered; the award travels with the signal, after it is banked.
#209 the prologue's charted sectors go with the breach, so the map the
survivors keep is of where they now are - and "chart 30 sectors" is no
longer complete before they have flown anywhere.
#218 enemy variety is gated on the FLEET, not on wave strength: a starting
fleet meets chasers only at any wave number, and each vessel past three
opens one more kind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
#204 the Beam Collimator widens the beam instead of adding range to one that
already crosses the screen; beam width becomes a per-shot vessel stat.
#205 the Energy Propulser gains three upgrades - dash recharge, dash reach,
and boosted-cruise drain - which turns three constants into vessel stats.
#206 three further doctrines carry the fleet from ten to twenty.
#207 the prologue's World Ship gets a marker, through the scanner's own
guidance arrow rather than a second one.
#208#210#213#214 four new synthesised sounds: the breach opening, cargo
reaching the hold, a rock breaking, and a gas bomb blooming.
#215 building health bars are larger, plated and framed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD