Commit graph spaceship/controllers
Author SHA1 Message Date
Claude
34e252037e #231, #230: a key per ability, and an upkeep sheet that adds up
#231 - the abilities were hard to use on a keyboard.

"The currently selected weapons" names a real mechanism: MouseCombatController
(#80) has number keys that SELECT which weapon a click fires. That is two
acts for one shot, and the selection is invisible unless you look at the
buttons. Q, E and space now fire the gas bomb, the laser and the dash
outright, wherever the mouse already points.

It goes through fire_at(), the same public entry the click path uses, so
every guard comes with it - no hull ready, cannot afford the shot, a panel
over the button. The number keys stay: they are not what this ticket is
about, and removing them would take the click path's abilities with them.

Q and E were already taken - they pivot the formation - so the pivot pair
moved to Z and C, one row under the movement keys and still left-hand. That
is a change to a control the ticket did not mention, and the one thing here
worth arguing with.

test_keyboard_input spelled Q and E out and so reported a broken pivot when
the pivot was fine. It reads KEY_PIVOT_CW/CCW off the controller now, and
additionally asserts the pair is not bound to an ability key - the failure
that would actually matter.

#230 - the upkeep menu never added anything up.

Two faults, and the second is invisible from the code. There was no TOTAL,
so the bill had to be done in the player's head. And the rows were one Label
padded with %-16s, which aligns only in a monospace font - the theme's is
CarroisGothicSC, which is proportional - with autowrap on top, so a long row
folded and the table lost its shape.

A real GridContainer now: header, a row per paying berth, and a TOTAL. The
headline carries both figures, because they answer different questions - the
per-charge bill is what the player watches happen, the per-minute rate is
what they plan against.

Amounts are listed in the game's resource order rather than the dictionary's.
The screenshot showed "2 ore 1 energy" on one row above "2 gas 2 ore" on the
next, which defeats the point of a column.

The sheet is rebuilt only when the numbers change. BuildController refreshes
this menu every frame while it is open, and rebuilding a dozen Labels per
frame for figures that move every 25 seconds is what #6 and #10 are about.
The test refreshes twice and asserts the same cell objects come back.

Every figure still comes from fleet_upkeep_breakdown(), the calculation the
charging loop itself runs - the new total sums those same per-berth costs
rather than recomputing them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-15 23:44:17 -04:00
Loïc Wisniewski
a43fae5e28 Let the speed fader have the click it is under
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
2026-09-12 19:06:29 +02:00
Loïc Wisniewski
93bfe62e76 Let the fleet grow past twenty vessels
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
2026-09-12 19:06:29 +02:00
Claude
f1292db40a #228: shrink the HUD on a screen the canvas is short on
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
2026-09-12 18:04:00 +02:00
Claude
fb3587173d #225: save the run, and offer to resume it
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
2026-09-09 21:32:23 +02:00
Claude
840f9aa9a0 #224: refuse to build a structure on top of another
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
2026-09-09 12:57:28 +02:00
Claude
37e2e89cd3 Stop the first play of each sound paying for its own load
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
2026-09-09 11:40:28 +02:00
Loïc Wisniewski
7d1a584c15 Stop redrawing what only needs moving
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
2026-09-08 11:35:45 +02:00
Loïc Wisniewski
8462099068 Teach where research data comes from, and say so when it runs out
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
2026-09-06 16:24:09 +02:00
Loïc Wisniewski
f77596395b Plan every craftable vessel, and stop restating the point cost
#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
2026-09-05 23:13:23 +02:00
Loïc Wisniewski
2bb46cba94 Charge more for each berth, and show the bill
#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
2026-09-05 22:36:21 +02:00
Loïc Wisniewski
265e121cb0 Build the logistics crews, and let them burn energy
#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
2026-09-05 19:09:35 +02:00
Loïc Wisniewski
78836882af Sharpen upgrades, mark the convoy's destination, and give the world sound
#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
2026-09-05 12:16:17 +02:00
Loïc Wisniewski
2239235993 Aim the laser by depth, and ease the aim view
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 09:32:42 +02:00
Loïc Wisniewski
a902fddba0 Show the laser's whole reach while aiming
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-04 23:20:29 +02:00
Loïc Wisniewski
1cb8fc54e9 Sharpen the gas bomb and the laser
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-04 22:34:29 +02:00
Loïc Wisniewski
fd792e8b63 Smooth boosted fleet travel 2026-09-04 20:01:56 +02:00
Loïc Wisniewski
8fbcc340ce Add planet scanner and octopus biome 2026-09-04 15:56:07 +02:00
Loïc Wisniewski
66b314f51a Expand and stage boss encounter 2026-09-03 22:00:26 +02:00
Loïc Wisniewski
40f284dcdd Add tentacled boss planet encounter 2026-09-03 10:33:05 +02:00
Loïc Wisniewski
0f7edff15a Complete lab, quest, garrison, and energy tickets 2026-09-03 00:12:08 +02:00
Loïc Wisniewski
3975959017 Improve aim assist target stickiness 2026-09-02 17:15:12 +02:00
Loïc Wisniewski
c2ebbfaa2d Resolve pending gameplay tickets 2026-09-02 13:43:56 +02:00
Loïc Wisniewski
7892a162f3 Use fire stick magnitude for aim distance 2026-09-01 23:50:05 +02:00
Loïc Wisniewski
a17fe88197 Refactor gameplay menus into owned scenes 2026-08-31 02:18:08 +02:00
Loïc Wisniewski
0fad1dd155 #166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.

#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.

#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.

#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.

#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.

#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.

#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.

#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.

#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.

test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.

The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.

Full suite green at 154/154.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
Loïc Wisniewski
3fcc26c8bc Implement #160, #161, #162, #163, #164 and #165
#162 - a single tap bought a research node outright. One physical tap arrives
TWICE: Godot emulates a mouse button from every touch AND a touch from every
mouse button, both project settings are on, and the menu's dispatcher routes
InputEventScreenTouch and InputEventMouseButton alike to the tree. The second
arrival found the node already selected and took itself for the confirming
tap. Measured, not reasoned about - one tap produced two picks and two
confirms.

What is ignored is specifically the TWIN: same frame, same place, arriving
through the other input kind. Deduping on frame and position alone also
swallowed two genuine taps in one frame, which the test caught.

#164 - the build menu predicted a caption's height from the COLUMN WIDTH:
wide column, assume one line. Right for most names and wrong for the long
ones, so "Spacial Workshop" wrapped onto the price beneath it. Captions are
now measured against the font that draws them, and prices sized from
PriceTag.wanted_height(), which existed for exactly this and was not being
asked. Prices are set before anything is measured, because the old order laid
out first and priced afterwards.

The ship lab: its buy button is gone and a second tap on an option buys it,
the same rule as the research tree; the explanation drops into the room that
freed; research points move up into the resource table, where everything else
being spent already is; the second row of options is placed from the first
row's measured bottom rather than a constant; and the vessel strip is spaced
from the panel's own width, so the eighth vessel is no longer half off the
right edge.

#160 - the quest panel was drawn BEFORE the sticks and the pivot pair, so
they sat on top of it. Moved after them, under the narration banner. Its title
rule ran straight through the close button; the header band is now as tall as
that button.

#161 - the prologue widens the formation into a freight convoy by writing over
GameState.formation, and nothing put it back. The player flew the convoy's
600-unit spread for the rest of the game - a shape nobody chose, which reads
as the fleet having drifted apart.

#163 - a quest to station a vessel at the outpost, after the one that asks for
the outpost. Building one and crewing it are different acts, and an empty
outpost holds nothing.

#165 - the gas bomb's blast and a vessel arriving fast both damage a
structure. The blast used to stop politely at anything the player owned, which
made a bomb dropped on a besieged mothership a free way to sweep the attackers
off it.

Collisions are caught on ENTRY, not on the 0.7s strike tick: a vessel is only
moving fast for the instant it arrives, and a ship sent in at 900 did no
damage at all until this moved. Docking is free - the fleet sits on its own
mothership constantly to bank ore - and garrison vessels are exempt outright,
because they launch from their outpost and re-dock with it at speed. The suite
caught that one: the ram rule was destroying the garrison as it launched.

Suite 153/153. Four tests changed because what they pinned moved: the lab's
buy button, the intro's formation, the research tree's tap delivery, and the
build menu's own preview.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 02:27:44 +02:00
Loïc Wisniewski
b721760803 #149, #153, #154: dialogs arrive, sit on top, and give the keyboard back
#149 - a line is typed out at about three and a half words a second, with a
scan sweep running down the panel and faint rules across it while it
decodes. The frame carries the charge while it works and settles when the
line lands; the caret is a solid write-head during the decode and only
starts blinking once it is waiting on the reader.

Two implementation notes worth keeping. The full text is set and then
REVEALED with visible_characters rather than the string being grown - a
growing string re-wraps on nearly every frame, so words jump between rows as
they are typed. And a press on a line still arriving FINISHES it instead of
dismissing it: without that, a player clicking at their usual pace skips the
second line having never seen a word of it, which is the exact thing #127
exists to prevent.

#153 - "after a dialog popup the input system is lost on keypress". Nothing
was lost; it was never delivered. A dialog stops the tree (#128), a paused
node receives no input, so a key RELEASED while the line was up never
reached the controller - keys_down kept it and the fleet flew on in that
direction once the game restarted.

NOTIFICATION_UNPAUSED arrives whatever a node's process mode is, which makes
it the one moment the question can be asked, and the physical keyboard is
the authority on what is actually held. That fixes anything that stops the
tree, not only dialogs, and the test checks it that way.

#154 - the banner was the ninth child of the UI layer and every panel came
after it, so all twelve of them drew over the top. It is the last child now,
in both scenes. Input order was never the problem and does not change: the
UI layer already sees unhandled input before the controllers do.
2026-08-29 14:39:07 +02:00
Loïc Wisniewski
080e3bb928 #147: one resource per line, and the price back under its button
Both halves of the report had the same cause: a craft cell laid out with
more slack than its row pitch had room for.

The caption box was 62px for a one-line vessel name, which pushed the price
70px clear of the button it belonged to - "far from the button they are
about". And the price was given 184px of width in a 190px column, so a
two-resource price ran out of its own box and into its neighbour's.

Prices now put each resource on its own line. Not only where they would
otherwise overflow, which was my first attempt: two resources came to 174px
in a 184px box, so it fitted - until the player was holding three figures
rather than one, and then it did not. A layout that is correct until you get
rich is not correct, and the ticket asked for the line break outright.

The caption is one line tall now with the price directly under it, and rows
are placed off a MEASURED band rather than a written-down pair of tops - the
same fix #144 needed for the build menu, and now the same code. A
centre-anchored grid under a top-anchored header has a gap between them that
is half the panel height, so it can never be a constant.

The test pins what the ticket describes rather than the numbers behind it: a
multi-resource price is stacked, its content stays inside its own box, it
sits within 80px of its button, and no craft entry overlaps another - the
price of one row into the button of the next included, which the old
geometry did by 14px whatever was priced.
2026-08-29 13:05:46 +02:00
Loïc Wisniewski
aabd5804cf #148: the main theme plays, and keeps playing
"All the time" is the whole of the requirement and it decides the shape.
The theme has to survive the main menu becoming the game, the game becoming
a planet surface, and the surface becoming the game again - so the player
cannot live in any of those scenes, because change_scene_to_file() frees
whichever one is current.

It is parented to the tree root instead, beside the GameState autoload,
which is the same trick for the same reason. Music.ensure() is idempotent by
node name, so each scene asks for the music on the way up and gets the one
already playing rather than a second copy over the top of it. Two call
sites cover everything: MainMenu, and GameController - which is the root
script of BOTH the space scene and the planet surface.

Done this way rather than as an autoload deliberately: project.godot carries
the developer's own uncommitted window settings, and adding music should not
drag those into a commit.

It is also PROCESS_MODE_ALWAYS. Since #128 a dialog stops the tree, and a
soundtrack that cuts out every time someone reads a line is worse than no
soundtrack.

The theme ships as MP3 rather than the 59MB WAV that was sitting in the
project root. Same 5m36s, 6.7MB, encoded with LAME at 160k - 89% smaller,
which is the difference between a repository that carries its music and one
that carries a WAV forever in its history. The original file is untouched
and still there, untracked, so nothing is lost.

The test checks CONTINUITY rather than "a sound started": the same player,
never a second one, through two scene changes and a dialog.
2026-08-29 13:05:21 +02:00
Loïc Wisniewski
2206b55873 #145, #144, #143: a quest that could not be finished, and two boxes that overhung
#145 - the extractor quest could never be completed. Extractors are placed
onto a planet from space and live in GameState.extractors, with their own
position, stock and id; they are not in `buildings`. So the goal asked
has_building_of_type("extractor"), which is false however many the player
builds. Not the death the reporter guessed at - the quest was simply asking
a question that had no yes.

structure_exists() is the question anything wanting "does the player have
one of these" should ask, and the quest model asks it now. The regression
test builds every structure quest's target THE WAY THE GAME BUILDS IT and
watches the goal flip, rather than knowing which array each one lands in -
the knowing is what got this wrong in the first place.

#144 - the build menu's status line is anchored to the top of the panel and
its grid to the panel's centre, so the gap between the two is half the panel
height. It was a written-down constant, correct only at 1280 tall; at 1080
the grid started four pixels ABOVE the status line and drew through it.

Nothing about that geometry is written against a screen size any more. The
band is measured from the header that is actually there, the pitch closes up
to fit it (never below a row's own height, which is what makes overlap
impossible rather than unlikely), and the column count rises until the rows
fit - so a short window gets a wide grid instead of one running off the
bottom. Ten structures now lay out as 5 columns at 900px tall, 4 at 1080, 3
at 1280 and 2 at 1600, clearing the header at every one, which is what the
test walks.

#143 - a hangar caption is "Gas Bomber\n2 available": two lines before
autowrap and three after it in a narrow column, at a size fixed in the
scene. Labels do not clip by default, so the third line was drawn over the
icon of the row below. The size is fitted to the box the layout works out,
and the box clips - the first keeps it legible, the second keeps it correct
however the vessel names grow.

Its box also overhung by exactly HANGAR_LABEL_GAP: the layout subtracted one
gap where it needed two, above the caption and below it. Two pixels, but two
pixels inside the next row's icon.
2026-08-29 12:51:09 +02:00
Loïc Wisniewski
1ece202731 #138: structures and doctrines cost more than rocks, and more the deeper they are
Both halves of the ticket, in the shape vessels have used since gas bombers
and laser ships: a definition carries an ore figure and, when it wants more
than ore, a dictionary that IS the price. `building_costs()` and
`skill_costs()` answer it the way `product_costs()` already did, so there is
one question with one answer whatever is being bought, and every payment
path now goes through `spend_resources` rather than `spend`.

Seven of ten structures and thirteen of seventeen doctrines now want gas,
energy or dark matter alongside their rocks, and the totals climb with tree
depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530.

The constraint that shaped the table is REACHABILITY, and it is the reason
some prices stayed ore-only. Energy exists only where a gas burner stands,
and a gas burner needs Power Systems - so nothing at or before that depth
may be priced in energy, or it is a wall with no way through. Dark matter is
rarer still and appears once, on the deepest node in the game. The extractor
stays rocks-only despite its depth allowing gas, because the quest chain
reaches it before it has sent the player for any.

test_progressive_costs checks those as properties rather than transcribing
the numbers, so rebalancing a price costs nothing while breaking the shape
fails: nothing unreachable at its own depth, prices rising with depth, the
ore field agreeing with the dictionary (repairs quote the field alone, since
a repair drone carries ore and nothing else), and the quest chain never
asking for a structure it has not funded.

Two things cleaned up on the way:

- The per-building cost constants in GameState are gone. Nothing but tests
  used them, and they were a second copy of a number the definitions already
  hold - exactly what would drift once a price gained a resource.
- research_for_building() returns a display NAME, and the id-keyed half of
  the tree wants an id. That mix-up is silent - skill_depth() of a name finds
  no node and answers 0, so a deep structure reads as an ungated one, which
  is how the first draft of the test let an unreachable price through.
  research_node_for_building() is the id, and the name is expressed from it.

PriceTag no longer rebuilds its labels when re-told the same price; it
refreshes the held figures instead. The craft menu re-states prices every
frame and the build menu now does too, so that was a dozen Labels being torn
down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
Loïc Wisniewski
ea25318201 #136, #137, #134: one build grid that cannot stack, one way to show a price
#136 - the gas burner drawn on top of the transport hub. The build menu's
rows were a written-down list of tops read with `tops[mini(row, size - 1)]`,
and with every structure researched the menu wanted a fourth row where the
list had three, so the last entry was placed on the third row's centre slot.
A list that silently stacks whatever will not fit in it is not a layout.
Rows are computed now, from a first top and a pitch that tightens to fit and
is floored at the row's own height - so two rows cannot occupy the same
space however far the catalog grows - and the column count widens rather
than adding a row the band cannot hold. Ten structures now sit in four
columns of three rows. The test unlocks the whole tree and checks every pair
of entries for overlap, which is the shape of the bug rather than a
coordinate that happens to be right today.

#137 - the game had three answers to "what does this cost". The build menu
drew a NeonNumber under each option, the craft menu drew glyph strips, and
the ship lab spelled prices out inside the caption. PriceTag is that answer
once: the resource's glyph, then what you HOLD over what it COSTS. It
replaces all three, plus the info popup and the skill panel, and
VesselPriceStrip - which was the same idea for one menu - goes with them.
The build menu also gains the resource bar the other two already had, in the
same place and at the same size, and the craft menu's - the only one not
running the full width of its panel - now matches. The bar drops its LOCAL
column where there is no building to report on, for the same reason #95
dropped it where it only repeated BANKED.

#134 - the skill panel's explanation and its price were both centred in the
same band, so a long description landed on the number. The text is the left
of the band now and the tag is the right. The shortfall it used to spell out
in a sentence - one per case, ore or research points - is the red on the tag
instead, and research points ride along as an entry of their own, so the tag
says WHICH thing is missing without a line of prose per possibility.

One trap worth recording: PriceTag and ResourceBar read the GameState
autoload, and an `extends SceneTree` test script that so much as names such
a class - or preloads the file - fails to compile, because GameState is not
an identifier in that context. GDScript then caches the failure, so the
scene the test instantiates gets a Control with no script where its resource
bar should be. The tests compare script PATHS, as strings.
2026-08-28 15:05:20 +02:00
Loïc Wisniewski
1e89780bd7 #128: a dialog stops the game until it is acknowledged
While a line is on the banner the whole tree is paused, which freezes the
fleet, the enemies and GameState's own production and wave clocks, and stops
input reaching any node but the banner. So a press while a dialog is up
acknowledges it and does nothing else - it cannot also steer the fleet or
open a menu on the way through.

The pause is owned by NarrationBanner rather than by its callers: the thing
that must not get out of step is "a line is showing" and "the game is
stopped", and the banner is the only code that knows the first. It releases
on the last line, on clear(), and on leaving the tree - a scene swapped out
mid-line must not leave the game stopped with nothing left to start it.

Two consequences worth naming. A beat's deadline in the prologue is now game
time, so it can no longer run out from under an unread line; it remains the
way past a beat the player will not finish. And "the prologue leaves you
movement and nothing else" was applied by _process on the first frame, which
no longer comes - the prologue opens ON a dialog - so BuildController and
WorldMap apply it at _ready too.
2026-08-28 14:40:46 +02:00
Loïc Wisniewski
8412280e71 #118/#121: the speed fader and the formation editor under a mouse
Both read raw touches, and the project no longer turns clicks into them - the
same routing gap #93 fixed in the research tree. The boost was unreachable on
a desktop and the formation could not be rearranged at all.

Wrappers over the same press/drag logic in each case, never a second
implementation, so the touch and mouse paths cannot drift apart. The wheel
replaces the pinch in the formation editor: a mouse has no second finger, and
zoom is the other half of what that panel does.

The fader gets first refusal on a mouse press in GameController, so dragging
the boost does not also pan or zoom the world behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:59:24 +02:00
Loïc Wisniewski
209169206b #108: the prologue - the convoy, the ambush, and the breach
Landed as one piece across #122, #123 and #124: the frame is useless without
a script, and the script has nowhere to run without the frame.

The director drives the REAL scene rather than being a scene of its own. It
wants the real fleet, the real enemies and the real asteroid field, and it
makes "then the game starts" cost nothing - the director stops steering,
clears the flag, and what is left running is the ordinary game.

The script is a list of beats, each with a deadline as well as its own
completion test, so a beat whose condition is never met - a player who
refuses to fly at the ambush - runs out rather than wedging the prologue.
skip() ends the whole thing at any point and lands in exactly the state
playing it through does, so the game has one way to start rather than two
that have to be kept in step. The test pins that equivalence.

"No action button available, only movements" is enforced where the buttons
are decided, not by the director reaching in: the ability controllers set
their own visibility every frame and would have put them straight back, and
the map button belongs to the map. The joystick, the pivot and the fader
stay.

The convoy is ten hulls where a new game's fleet is one, so it needs an
explicit fleet-size override - a plain number rather than a fake doctrine,
because nothing about the player's own progression should be bent to fit a
cutscene. It is carriers rather than transporters: transporters belong to
hubs and are not fleet-eligible, so a convoy of them would be silently
filtered down to its escorts.

The ambush is chasers and only chasers. "Easy enough" is the requirement,
and a sniper against a convoy the player cannot yet fight properly is
neither easy nor a good first impression.

The breach takes the wheel through one check in GameController, covering the
stick, the keyboard and the released-again path together - three separate
gates would be three chances for one of them to keep steering. The shake is
written to camera.offset, never to its position, because GameController owns
where the camera is and would smooth a shake away.

The vessel that survives is chosen as the interceptor rather than whichever
happened to be first in the list, and the prologue ends on exactly a new
game's opening position.

Also: PlanetAtmosphereController no longer spawns the opening fleet while
the prologue is arming, and the tests that are about what the GAME starts
from now say so by turning the prologue off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:59:12 +02:00
Loïc Wisniewski
b50597b757 Garrison laser vessels and gas bombers, on a shared ability seam
#105 shipped with interceptors as the only garrisonable vessel, and the
reason was a design fault rather than a decision: firing the laser and the
gas bomb lived in the player's fleet controllers, so a stationed laser would
have held its position and never shot.

The shot is now a thing in the world rather than something a controller
draws. LaserBeamEffect and GasBombEffect are self-contained, self-animating
and self-freeing, added to the tree root - the same shape EnemyBigLaserBeam
and ArtilleryShell already had on the enemy side - and they outlive the
vessel that fired them.

Above them sits one seam, on VesselCooldownFillComponent: ability_range(),
ability_ammo_resource(), ability_ammo_cost() and fire_ability(). A beam and a
bomb are now the same shape of thing to whoever pulls the trigger, so the
garrison AI never names an ability - the test asserts that literally, by
reading the AI's own source. A third armed vessel needs to implement the
seam and nothing else.

Ammunition is the CALLER's business, deliberately: the fleet spends from its
hold, a garrison from its outpost's store, and neither concern belongs in
the component. A garrison with an empty magazine holds fire rather than
falling back on the fleet's hold.

Outpost storage: 50 gas per stationed bomber and 50 energy per stationed
laser, off the VESSEL definitions, so an outpost never learns which vessel
needs what. It is a capacity, not a stock - hub transporters fill it exactly
as they fill a gas burner's buffer, because building_logistics_deficit now
takes the larger of the definition's static buffer and the garrison's
magazine. Every other resource is capped at zero: an outpost is not a depot,
and ore parked at one has nothing to do there and no transporter coming for
it.

Also restores two properties the Godot editor dropped from
GasBurnerBuildingDefinition.tres when it re-saved the file:
collected_outputs and capabilities. Without the first, nothing ever collects
the burner's energy - test_gas_burner caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:23:35 +02:00
Loïc Wisniewski
0c595d775d #106/#107 (#98a/#98b): the Planning Center
Landed together: a board of standing orders nothing acts on is a notepad,
and a planner with nowhere to read the plan from has nothing to do.

The targets are GLOBAL, not per building. There is one plan for the whole
game, and a second planning centre is another place to read and edit it
rather than a competing one - per-building targets would let two centres
hold contradictory orders and tell the crafters to satisfy both.

The census is the harder half. Every vessel is counted exactly once wherever
it happens to be: flying in the fleet, parked in the hangar, stationed at an
outpost, or in the air between any two of those. Miss the in-flight ones and
the planner queues a replacement for a vessel that is merely en route. The
fleet is the one place GameState cannot see, so Fleet pushes its composition
on every change, alongside the cargo capacity and distorder count that
already went that way.

Three rules keep the planner from fighting the player. It tops up and never
rewrites, so an order placed by hand stays exactly where it is. Everything
counts, including what is already queued, or it re-queues the same shortfall
every tick and buries the player in hulls. And nothing happens without a
planning centre - though the plan is remembered when one is destroyed, so
rebuilding picks up where it left off.

Vessels on order are counted separately from the census on purpose: they do
not exist yet, so the readout can say "4 of 6, 2 building" rather than
claiming six. The per-row note says where the vessels actually are, because
a plan can look unmet while everything it asked for is sitting at an
outpost.

The build menu goes to three columns past eight entries. #54 already
tightened its rows to 223px to fit a fourth, explicitly to avoid a
scrollable menu, and a ninth structure needs a fifth row that will not fit
at any spacing a button, a label and a cost can share. Three columns take
nine into three rows - fewer than it uses today, not more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:21:52 +02:00
Loïc Wisniewski
3c632276af #102/#103 (#97d/#97e): the outpost, and the garrison you compose for it
Landed together because an outpost you cannot crew is decoration, and a
garrison menu with no building to hang it off is nothing at all.

#102, the building. Researched at "Forward Bases", two prerequisites deep
and cheaper than any mid-game structure - the ticket asks for early, and the
answer to waves attacking your buildings should arrive at about the time the
waves can hurt them. It is drawn as a watch mast on a tripod with two open
berths, deliberately the least machinery of any structure here: it makes
nothing and refines nothing, it holds a position. 120hp, more than a
crafter, because being shot at is its job.

#103, the garrison. Two separate things live on its record, and conflating
them was the first thing to get wrong: the ORDER, what the player asked for,
and the CREW, who is actually there. This menu edits the order only; #104
fills the crew. Keeping the order as a wish is what lets an outpost ask for
a hull nobody has built yet and wait for it, rather than the request
evaporating - and it is why the panel shows "here / ordered" instead of one
number.

"Only ships that attack can be assigned" is expressed as capabilities rather
than a list of types, so a new armed vessel is eligible the day it is added
and an unarmed one never is by accident. Today that means the interceptor
and the laser vessel; a shield, a hauler and an engine block cannot hold a
position however much one might want them to.

Two berths by default, and Garrison Bays raises it at outposts ALREADY
standing - the same shape as freight_yards for hubs, because a bonus that
only reached new buildings would mean demolishing and rebuilding to collect
it.

The panel builds its own rows from the catalog rather than from nodes placed
in the scene, so adding an armed vessel later needs no scene surgery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:02:20 +02:00
Loïc Wisniewski
c8d4731232 #90: give the laser an ignition instead of a blink
The shot was one flat line that appeared at full length and faded. Nothing
happened at the ship, nothing moved, and a hit looked exactly like a miss.

The blade now runs out of the hull over the first 30% of the flash on a
cubic ease-out, holds white-hot, then falls away, with a hum in its width,
a bright cap at the leading end, a flare at the emitter, and sparks off the
hull that fired it. MuzzleSparkVFX takes an optional colour so the laser
throws cyan where the ordinary gun (#87) keeps its gold.

The extra light is a white-hot rod down the middle at 42% of the width,
deliberately not a wider halo: the note at BEAM_WIDTH_SCALE records that the
drawn beam spans exactly what the beam hits, and a halo past that would put
the "it visibly missed" complaint back the other way round.

The damage is untouched - dealt in full, along the whole beam, on the frame
of the shot. A beam that had to arrive would change how the weapon plays,
which the ticket did not ask for, so the test pins it down: a target at the
far end of the range must be hit before the drawn blade has visibly reached
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:29:55 +02:00
Loïc Wisniewski
4883197ef3 #93: drive the research menu with a mouse, and close the hole under it
Two separate faults, both reported on the same ticket.

The menu was inert under a mouse. Input reaches the tree as raw events
through BuildController._unhandled_input, because a Control that consumes
them through the GUI system swallows the drag, and the tree only ever read
touch events. That was invisible while the project set
emulate_touch_from_mouse and every click arrived as a synthetic touch; with
that setting gone, nothing selects and nothing scrolls, and there is no
other way to research anything on a desktop. The press and the pan are now
the behaviour, with touch and mouse as two ways in to them. Motion pans only
while the button is held - a hovering pointer must not drag the tree out
from under the reader - and the wheel scrolls, which is the first thing a
desktop player reaches for.

The panel also still had the buy button's hole in it. That button went away
in #83 and nothing moved down into the space, leaving ~180px of dead panel
below the readout while the tree above it stayed cramped. The readout moves
down 154px and the canvas takes the height: 1190px to 1344px.

The test drives clicks through BuildController rather than into the view,
since the missing piece was the routing - calling the view directly would
have passed against the bug as reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:29:34 +02:00
Loïc Wisniewski
5f3615026d #89: the default gun bends onto a target near the aimed heading
Aiming with a thumb on a stick is never exact, and at 950 units a couple of
degrees is a long miss. A shot fired roughly toward an enemy now goes at it.

28 degrees of cone, chosen so that pointing at something hits it while the
shot still visibly goes where the player pointed. Past about 45 the gun starts
picking its own targets, which is the behaviour #80 deliberately removed.

Enemies only, never asteroids. Snapping onto a rock while the player is trying
to hit the thing shooting at them would be the assist working against them,
and rocks are large and stationary enough to hit on purpose.

The aim guide is drawn through the SAME helper the gun fires through, so the
dotted line bends with the shot. A guide showing the raw stick heading while
the bolt took an assisted one would be the same class of mismatch #86 removed
between the guide's length and a bolt's actual reach - and this one would have
been harder to notice, because the line would look right most of the time.

Range 620 -> 950, the other half of the ticket.

Also swept the project root again: Godot keeps resurrecting pre-#84 copies of
moved files there from its own cache. None this time.

Suite 120/120.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 03:19:52 +02:00
Loïc Wisniewski
3f5e7ac80b Implement #85, #86, #87 and #88
#85 - on a computer the sticks and the pivot pair are dead weight: WASD
steers, the mouse aims, Q/E pivot. All four go together the first time a
keyboard or a mouse is used, and only where there is no touchscreen. A touch
device with a keyboard attached keeps everything, because a finger is still a
way to play it.

#86 - the default gun shoots faster (500 -> 850) and further (320 -> 620).

The interesting half is that a bolt now STOPS where the gun's range ends.
Projectile lifetime is derived from fire_range at launch rather than set
independently, so the distance a shot covers and the distance the aim guide
draws are one number. They were two, and they disagreed: the guide promised
320 while a bolt actually flew speed * lifetime = 750, so shots landed on
things well past the line saying where they would stop.

#87 - the ship that fires throws sparks, so a shot is visibly attached to the
vessel it came from; with a whole fleet shooting there was nothing tying the
two together, which started mattering once the player had to aim. Enemy deaths
gained a shockwave ring and roughly double the debris - a kill used to read as
a small red puff that was easy to miss in a fight.

#88 - everything moves 1.5x faster: fleet cruise, every vessel's max_speed and
acceleration, and every enemy's. Scaling the enemies too keeps it a change of
TEMPO rather than a difficulty cut, and scaling acceleration alongside speed
keeps the handling from #77's tuning intact.

Three tests carried assertions that were really about one balance pass:
test_enemy_mothership pinned a literal 35.0 and now compares against the
chaser, which is what "never reaches ramming speed" actually means;
test_planet_atmosphere_transition flew far enough at the new speed to reach
the landed garrison, and now clears it, since that test is about the handoff
and not about a fight it never meant to pick.

Also repaired real data loss, caught only because test_gas_burner failed:
Godot had silently re-serialised GasBurnerBuildingDefinition.tres and dropped
collected_outputs, capabilities, map_radius and map_phase, and had recreated a
stale copy of it at the pre-#84 root path. Without collected_outputs no
transporter ever collects a burner's energy. Both files restored from git.

It had also resurrected pre-#84 SNAPSHOTS of Main.tscn, Planet.tscn and
lunar_surface.gdshader at their old root paths - the root Main.tscn still
carried 27 references to the flat layout. They are dead weight the engine
never loads, since project.godot and every preload point into the folders, but
they are exactly the kind of thing that wastes an hour later when someone
edits the wrong copy. Deleted, and the root is now down to ui_theme.tres,
icon.svg, web_shell.html and the two config files.

Suite 119/119.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:20:12 +02:00
Loïc Wisniewski
2f161621c3 #84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it
touched and finding the file you wanted meant knowing its exact name. Twelve
folders now, grouped by what a thing IS rather than what type it is - a
vessel's definition, its component scenes and its upgrade resources sit
together, because that is the set you read when you change one of them.

  buildings  core  controllers  enemies  logistics  planets
  resources  shaders  surface  ui  vessels  vfx  world

332 files moved and every res:// reference rewritten - 306 files touched,
including the autoload and main-scene paths in project.godot. Nothing renamed:
this is purely where files sit, so the diff is a pile of pure moves and no
behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at
the root, where Godot and the export preset expect them.

Verified three ways, because a suite alone would not have caught all of it:
the full 117 pass, the web export builds clean, and the game renders.

The export is why that matters. The FIRST export after the move failed with
"Attempt to open script 'res://GameState.gd'" - the old path - while
project.godot on disk plainly said res://core/GameState.gd. That is this
project's documented Android FUSE mtime trap: Godot served a cached copy of a
file whose content had changed. A `touch` fixed it. Nothing in the test suite
would have found that, because the suite runs from a copy on a native
filesystem - the one place the bug cannot happen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00