Commit graph spaceship/tests
Author SHA1 Message Date
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
Claude
693919dadb Let a dialog opened over another dialog be closed
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
2026-09-08 22:31:56 +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
54b883d545 Keep the export probe, and the uid files beside the new scripts
_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
2026-09-07 16:40:45 +02:00
Loïc Wisniewski
df4d5e35f9 Find the planet setups in an exported build
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
2026-09-07 13:55:41 +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
aca04ad4cf Teach the crews and the berths the game now asks the player to keep
#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
2026-09-06 14:43:35 +02:00
Loïc Wisniewski
296b49c8b8 Offer only orderable vessels, and read out crew condition
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
2026-09-06 11:58:15 +02:00
Loïc Wisniewski
fe3ae97f94 Stock a Fleet Office for its own berth, and stop there
#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
2026-09-06 10:44:30 +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
cc71811d89 Research the Fleet Office, and give it one hull
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
2026-09-05 21:57:53 +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
1f5fecd371 Buy fleet berths with Fleet Offices
#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
2026-09-05 17:09:27 +02:00
Loïc Wisniewski
6e13b6397c Take planets by killing what defends them
#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
2026-09-05 13:31:25 +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
571f01dc31 Add a free-costs sandbox tool
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-04 22:34:36 +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
10d19a369b Guide the fleet to the nearest unconquered scan target
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-04 21:46:24 +02:00
Loïc Wisniewski
fd792e8b63 Smooth boosted fleet travel 2026-09-04 20:01:56 +02:00
Loïc Wisniewski
e5439f7183 Refine boss reveal and dash motion 2026-09-04 18:40:58 +02:00
Loïc Wisniewski
8fbcc340ce Add planet scanner and octopus biome 2026-09-04 15:56:07 +02:00
Loïc Wisniewski
2ecd61de98 Restrict speed boost to space 2026-09-03 23:14:02 +02:00
Loïc Wisniewski
9864e59efc Arrange fleet abilities around fire stick 2026-09-03 23:00:52 +02:00
Loïc Wisniewski
cb6b0b69c6 Make boss encounter more dynamic 2026-09-03 22:31:32 +02:00
Loïc Wisniewski
471a43e8a6 Fix boss planet landing controls 2026-09-03 22:21:11 +02:00
Loïc Wisniewski
66b314f51a Expand and stage boss encounter 2026-09-03 22:00:26 +02:00
Loïc Wisniewski
c325501999 Turn dev mode into sandbox menu 2026-09-03 20:05:33 +02:00
Loïc Wisniewski
68985316e5 Tune and optimize boss encounter 2026-09-03 11:49:51 +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
d413d1da9f Polish dialog and intro transitions 2026-09-02 20:18:44 +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
ec20778215 Enlarge mobile joystick controls 2026-09-02 11:19:54 +02:00
Loïc Wisniewski
7892a162f3 Use fire stick magnitude for aim distance 2026-09-01 23:50:05 +02:00
Loïc Wisniewski
eb1615ab6b Restore landing fade and missing event sounds 2026-08-31 18:15:37 +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
4c920f10b0 #157, #158, #159: pay from the hold you flew in, and nothing is exempt from coming down
#157 was already satisfied - second_hull is quest 7 and extractor is quest
11 - so this pins the property rather than adding a quest: nothing costing
more than a single hold may be asked for before the chain has grown the
fleet or taught banking. It fails if either is ever moved behind it.

#158 - a building's local costs may now be paid out of the fleet's hold.
#12 made production cost resources physically DELIVERED rather than money
out of the bank, and that stands: the bank is still not raided here. The
hold is simply the other place resources physically are, and making the
player park, open the transfer menu and move cargo across by hand was
ceremony rather than a decision.

But only WHERE THE FLEET IS, and that qualifier is the whole design. The
first draft had no range on it and test_manual_supply_route caught it
immediately: a crafter three thousand units away reached into the hold and
spent ore the player had just withdrawn to carry somewhere else - which is
exactly the complaint that got automatic banking removed in #24, arriving
from the other direction. The range is Building.interact_radius, the same
distance that decides whether the transfer button appears, so the rule is
not a hidden one: if you could hand the cargo over, the building can take
it.

#159 - the mothership floored at 1hp, so a battered one sat there reading as
dead and never exploded. That exemption existed to prevent a soft-lock, and
the soft-lock turns out not to exist: the bank is GameState.bank_inventory
and the hangar is stored_vessels, both of which outlive any building; the
mothership is the one structure research never gates; it costs exactly what
a single interceptor's hold carries; and ore comes from asteroids, which do
not run out. So the way back is always mine, fly, rebuild - and that is what
test_building_health now asserts, in place of the exemption that used to
stand in for it.
2026-08-29 15:51:34 +02:00
Loïc Wisniewski
7489528e60 #155, #152: shots reach further and sooner, and the other explosions catch up
#155 - the projectile flew at 850, which took 1.1 seconds to cross the gun's
own range: long enough that a shot read as leaving the ship rather than as
hitting something. 1400 now, and the interceptor's range goes 950 to 1200,
so it crosses in 0.86s and comfortably outranges the 620 the standoff
enemies hold at. A projectile's lifetime is derived from its range and
speed, so faster does not mean falling short - the test pins that
arithmetic rather than trusting it.

#152 asked for VFX on three events, and the finding is worth recording: all
three already existed and were already wired. What had happened is that #130
gave a lost VESSEL thirty particles and a shockwave, which left everything
else in the game visibly smaller than it - an enemy kill, the thing the
player is being rewarded for, and a broken rock, the thing they do more than
anything else, were both still on or near the base class's twelve-particle
default with no shockwave at all.

So this is about PROPORTION rather than about adding anything, and
proportion is what the test checks: a lost vessel outweighs a kill,
a kill outweighs a rock, and nothing sits on the base defaults. Numbers get
rebalanced; an ordering that says a rock outweighs a battleship is a bug.
Particles now run 38 > 32 > 24 against a default of 12.
2026-08-29 14:39:35 +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
721913c040 Guard the definition catalog against silent .tres property loss
Opening the project in the Godot editor re-saves .tres files, and its writer
DROPS properties it considers default-valued. An empty-looking
PackedStringArray is the usual casualty, and it has now cost this project
twice - both times silently, and both times the damage surfaced somewhere
else entirely:

  GasBurnerBuildingDefinition lost `capabilities` and `collected_outputs`,
  which took the burner's whole conversion with it.

  InterceptorVesselDefinition lost `capabilities`, `hull_points`,
  `build_time`, `icon` and `glow_color` - so the starting vessel had no
  cargo hold, and a full suite came back with ten unrelated tests broken
  because nothing anywhere could mine.

Neither raised an error. The resource still loaded; it was just quietly less
than it had been.

test_definitions_intact asserts the SHAPE of the catalog rather than any one
value: every vessel has a name, stats, a hull and its components; a hold and
a "cargo" capability imply each other; every building has a description, a
price, hit points, its build-menu wiring and at least one capability; and a
converter keeps the fields its conversion needs. Whatever the editor eats
now fails here, next to the thing that was eaten.

Verified against both real incidents by reproducing them: deleting the
interceptor's capabilities line fails with "cargo_capacity 20.0 but
capabilities []", and deleting the burner's collected_outputs fails with
"converts but lists no collected outputs".

The first draft of that last check looked for the conversion fields on the
DEFINITION, where they are not - they live on its logistics sub-resource -
so it answered null and never fired. It passed while the burner sat there
broken, which is the same class of quiet as the bug it is guarding.

Also restores what the editor took from the gas burner this time round.
2026-08-29 14:38:51 +02:00
Loïc Wisniewski
b8038c1f94 #148: keep the theme's master in the repository
SPACE_4.wav is the source the shipped theme is made from, so it belongs
under version control - but not in the game.

It lives in audio/masters/ with an empty .gdignore beside it, which makes
Godot skip the folder outright: nothing in it is imported into res://, and
nothing in it is carried into an export. Left where it was, in the project
root, its 59MB would be imported and then packed into every Android and web
build for a file nothing opens - the game loads audio/space_4.mp3, the same
5m36s at 6.7MB.

Its .import file goes with it, since there is no longer anything to import,
and the test runner stops copying the folder for the same reason it already
skips android/ and builds/: 59MB copied on every single run, for a folder no
test can even see.

README.md next to it records which shipped file each master became and the
command that made it, so the pair cannot drift apart silently.
2026-08-29 13:54:37 +02:00
Loïc Wisniewski
940540d60c #146: shots, deaths and collapses are heard
Three events get a sound: a shot leaving a gun, a vessel dying, a structure
coming down.

The sounds are SYNTHESISED rather than downloaded - audio/make_sfx.py
generates all three from scratch, and is committed alongside them. The
ticket asked for free sources and the good ones are in its comments
(Kenney's CC0 packs, OpenGameArt's CC0 sets, Freesound filtered to CC0),
but a generated sound has no licence to track, no attribution to keep, and
no binary in the repository whose provenance nobody can check a year from
now. It also makes the sounds DATA: too sharp a shot is a number in that
file and a re-run, not a hunt through a library. Dropping in a pack file
later means replacing a .wav and nothing else.

A voice POOL, not a player, because these overlap constantly - a fleet
firing is several shots inside one shot's length. Twelve voices, and when
they are all busy the one furthest through its own sound is recycled, which
is the one with the least left to lose.

Parented to the tree root, for the reason that matters: every one of these
sounds is started by something that is being freed on the same frame. A
dying ship cannot own the sound of its own death.

Levels are per sound rather than shared - a shot fires constantly and a
death happens once, so equal volumes are not proportionate ones - and shots
get a little pitch spread so a stream of them does not flatten into one
machine-gun note. The deaths get none: a wobbling explosion sounds broken
rather than varied.

The test goes at the failures a play-through would hide for weeks: firing
three times the pool size must leave exactly the pool's worth sounding and
no leaked voices, and the ship-death case is checked from a node that really
has been freed on that frame.
2026-08-29 13:05:47 +02:00