Commit graph spaceship/tests
Author SHA1 Message Date
Loïc Wisniewski
097e17f7aa Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.

The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.

The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.

Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.

Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.

Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.

Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
  matching target_id, regardless of purpose. Once pickup states shared
  target_id with delivery states (both address a building, not an
  extractor), a transporter delivering gas made the SAME building look
  "busy" to the unrelated energy-pickup dispatch, starving it even
  with a second, genuinely free transporter sitting idle at the hub.
  Split into _transporter_inbound_to() (delivery states only) and a
  new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
  sizing) had no room for a 7th building - despite the ticket's own
  note that no layout changes should be needed. A naive 4th row ran
  off the bottom of a 720x1280 viewport; fitting one meant both adding
  a 4th row top AND shrinking the shared button/label/cost sizing so
  each row's content no longer taller than the compressed row spacing.
  Caught only by actually screenshotting it - a real build menu, not
  headless, since --headless renders nothing.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
Loïc Wisniewski
472de1d93e Enemy mothership (#53)
A large, slow capital enemy that periodically launches chasers and
artillery on their own intervals and fires a heavily telegraphed line
beam capable of killing several player vessels at once.

Spawning reuses #52's EnemySpawnerComponent as-is via two preset
scenes (EnemySpawnerComponentChaser.tscn: 5s/chaser,
EnemySpawnerComponentArtillery.tscn: 18s/max 2/artillery) rather than
adding multi-type support to the component - two instances of a
single-purpose component is the composition this codebase already
uses everywhere ("enemy bomber" in the original request is read here
as the existing artillery enemy's slow telegraphed AOE, not a new
enemy type - flagged as a judgment call in the ticket).

New EnemyBigLaserComponent.gd ("big_laser") fires a standalone
EnemyBigLaserBeam on a long cooldown - the beam owns its whole charge/
fire/flash sequence once launched, so the mothership dying mid-charge
doesn't cancel an already-telegraphed shot, same design as
EnemyArtilleryComponent/ArtilleryShell. The aim direction is locked
once in launch() and never re-read - nothing in the charging phase
touches the fleet's position again, which is what makes it dodgeable.
Damage is a line-segment check (projection + perpendicular distance)
against every player_ships member; Ship.take_damage() already handles
shield absorption per-ship.

MothershipEnemyDefinition.tres: max_hp 60, speed 35 (slow), a ~96-unit
hull, pursuit + two spawners + big_laser, no type switch anywhere -
every stat from the definition, every behaviour from a component.

Tests: new tests/test_enemy_mothership.gd, split into beam mechanics
(tested directly, not through the component's own cooldown timing,
which would make the geometry assertions timing-fragile) and the
composed definition (tested through a real Enemy.tscn instance).
Screenshot of the charge telegraph mid-countdown. Full suite: 80/80.

Found and fixed a real bug along the way, in EnemySpawnerComponent.gd
(#52): its `enemy_scene: PackedScene = preload("res://Enemy.tscn")`
member-level preload could get permanently baked in as a broken,
empty PackedScene when a definition's component_scenes chain led back
to Enemy.tscn while it was still mid-load - confirmed empirically via
ResourceLoader.has_cached() being true (a good copy existed) while the
preloaded reference's own resource_path was empty. This only
surfaces when a SINGLE definition composes two instances of a script
carrying such a preload (the mothership's chaser/artillery spawner
presets; #52's single-spawner turret never hit it). Fixed by
resolving enemy_scene lazily via load() on first use instead of at
compile time, and additionally made a fresh spawner count down from a
full interval before its first spawn rather than firing on tick one,
which also happens to keep two sibling spawners with different
intervals from ever landing their first attempt on the same frame.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 10:19:02 +02:00
Loïc Wisniewski
1bd25c6d27 Turret vessel spawner enemy (#52)
New EnemySpawnerComponent.gd (capability_id "spawner"): continuously
instantiates another enemy type beside its host at a fixed interval,
capped at max_alive concurrently, refilling as spawns die. Spawned
enemies are parented under the SPAWNER'S OWN PARENT rather than the
spawner itself, so they are siblings, not descendants - killing the
spawner can't free them, and a surface-hosted spawner (under
PlanetSetupHost) keeps its spawns under the same host instead of
leaking them to a hardcoded path, the same hazard #44 already closed
for pursuit/standoff/ranged-weapon components.

TurretSpawnerEnemyDefinition.tres: stationary (speed 0, no pursuit or
standoff component), max_hp 20 (~7x a chaser), a bulky octagon hull,
and its own acid-green glow distinct from chaser/sniper/artillery.
Registered in EnemyDefinitions.ORDER/DEFINITIONS - the map editor
palette (#48) picks it up with no editor code change.

Tests: new tests/test_turret_spawner_enemy.gd covers registration and
real definition defaults, no spawns without a fleet, spawning capped
at max_alive, a dead spawn's slot refilling, never moving, surviving
19 damage and dying on the 20th, and its spawns surviving its death
with their pursuit component intact. Screenshot beside a chaser
confirms the two read as visually distinct enemies. Full suite: 79/79.

Repo notes for the next agent, confirmed empirically this session:
- A node's set_process(false) call made before it has fully finished
  entering the tree doesn't reliably stick (already documented in
  tests/test_planet_atmosphere_transition.gd, and it bit this test
  too): BuildController._check_fleet_wipe() kept rebuilding a ship
  during an intended "empty fleet" test window even after disabling it
  in _initialize(). Do it in _process()'s first real tick instead,
  once _ready() cascades are confirmed complete.
- Overriding an @export interval (like spawn_interval) on an
  already-running component does NOT reset its own in-progress
  countdown timer - only changes what it resets TO on its next expiry.
  If the countdown already started at the OLD value before your
  override lands, reset the timer variable itself too, or the first
  real interval after the override silently uses the stale value.
- A capped spawner's freed-slot refill can land within a couple of
  frames once its interval is short - don't assert on an intermediate
  "just dropped by one" count that a fast interval can blow straight
  through before your next check runs. Assert the net effect instead
  (the specific killed instance is gone AND a replacement filled the
  slot) rather than an exact mid-transition snapshot.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:43:44 +02:00
Loïc Wisniewski
4606c34439 Give the gas planet surface its own background shader (#51)
lunar_surface.gdshader recoloured was the whole gas surface backdrop,
so a landed gas giant read as regolith - the crater field never went
away, just changed colour. New gas_surface.gdshader replaces it for
gas profiles only: a nearest-row-wins horizontal band field (same
single-scalar discipline as lunar's nearest-crater field), with
drifting per-row turbulence so the bands ripple instead of reading as
ruled lines. Keeps the exact same uniform interface as lunar
(camera_cell/camera_frac/camera_zoom/viewport_size/cell_size/
base_color/crater_color/highlight_color), so LunarBackground.gd needs
no changes at all.

PlanetSurfaceProfile gains background_shader (default null = keep the
scene's existing lunar shader); PlanetSurfaceController._compose_surface()
swaps the background material's shader in only when a profile names one.
GasSurfaceProfile.tres points at the new shader and retunes its palette
to a deep-atmosphere/shadowed-band/lit-crest read; RockySurfaceProfile.tres
is untouched, so the rocky surface renders exactly as before.

Precision: every hash input is either an exact integer cell/row index or
a bounded [0,1) local fraction, same split as lunar_surface.gdshader and
Starfield.gd use for the same reason (world coordinates reach the tens of
thousands, where mobile float precision collapses). TIME-based lateral
drift is folded into the lattice coordinate via the same floor/remainder
trick rather than ever being added to a large cell index directly - see
combine_lattice()'s header note.

Tests: new tests/test_surface_profiles.gd (rocky resolves no
background_shader and keeps the lunar shader; gas resolves and applies
its own, different from lunar's). Full suite: 78/78 passed. Screenshot
of the gas surface confirms drifting bands with no craters, rendered
well inside the softpipe performance budget (20 frames in ~2m40s,
against lunar's "minutes per frame" baseline the ticket warns about).

Repo notes for the next agent, confirmed empirically this session:
- Never reference a plain (non-autoload) class_name class by its bare
  global name from a -s entry-point test script - `load()` it by path
  into a local var instead, same as PlanetSurfaceController.gd and
  PlanetSetups.gd already do for exactly this reason. Doing it the
  unsafe way here didn't fail to compile - it corrupted something in
  the resource-loading state badly enough that an UNRELATED scene's
  Rock.tscn lost its script attachment and RockField's rock-spawning
  loop exhausted the physics server's entire RID pool. If a test's
  crash trace implicates code you never touched, check every bare
  global class_name reference in the test first.
- Running a `tests/screenshot.sh` render (Mesa softpipe, genuinely
  minutes for a crater-shader scene) at the same time as a
  `run_tests.sh` full-suite run starves both for CPU on this device -
  kill one before waiting on the other rather than running them
  concurrently.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:05:31 +02:00
Loïc Wisniewski
0582181f26 Generalize the resource HUD readouts (#49)
Replaces ResourceCounter/GasCounter/CargoCounter/GasCargoCounter - one
hardcoded node plus one script per resource, times two scenes - with a
single ResourceReadout.gd that builds one banked + one fleet-hold row
per GameState.resource_types() entry at _ready(). A new resource added
to ResourceDefinitions gets a HUD row with no scene edit and no new
script, matching how ResourceBar and ResourceTransferList already
generalize over resources.

Preserves the existing visual layout (BANKED left, FLEET HOLD right),
the ore-on-bright-surface contrast (via a bright_background flag
replacing PlanetSurface's per-node color overrides), and the amber
fleet-in-transit color convention. Reuses the already-generic
ResourceGlyph for icons instead of NeonNumber's hardcoded ore hexagon.

Uncovered and fixed a second bug along the way: test_cargo_readout.gd
looked up its NeonNumber nodes immediately after add_child() in
_initialize(), but ResourceReadout builds its rows in _ready(), which
for a -s entry-point script does not fire synchronously during that
add_child() - it's deferred to the first frame. Moved the lookup to
after the existing elapsed wait in _process(), which is also what
tests/test_resource_readout.gd's setup does from the start.

Repo notes for the next agent, confirmed empirically this session:
- A -s entry-point script can only reach an autoload's CONSTANTS via
  the bare identifier (GameState.RESOURCE_ORE works everywhere). Calling
  any of its METHODS that way (GameState.resource_types()) fails to
  compile with "Identifier not found: GameState", even from
  _initialize(). Route method calls through the actual instance
  (root.get_node("GameState")) instead, as the rest of the suite already
  does.
- `git status --short | awk '{print $2}' | xargs touch` recreates
  deleted files as empty stubs, since touch on a missing path creates
  it. Filter out D-status paths before touching, or touch only the
  paths you know still exist.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 08:30:10 +02:00
Loïc Wisniewski
5b2dd77e47 Draw capture points behind fleet vessels (#50)
CapturePoint.gd left z_index at its default 0, the same layer Ship
draws on, so a ship parked inside a ring was partly swallowed by the
ring fill - exactly where the player most needs to see their own
vessels. Set z_index = -1, matching the convention every other world
object already follows (Planet, Rock, Building, Hauler, Transporter,
VesselDelivery).

Verified with a ship placed directly on a capture point in the
preview screenshot, and asserted in test_capture_points.gd.
2026-08-15 22:47:44 +02:00
Loïc Wisniewski
df2f252970 Add the planet setup map editor (#48)
PlanetSetupEditor authors PlanetSetup resources: pick a surface
profile (drives the real lunar_surface background, not a blank
sheet), place/move/delete capture points and enemies from a palette
generated straight off EnemyDefinitions.all() (a new enemy definition
shows up with no editor change), pan/pinch-zoom the canvas with the
same touch idiom as GameController, then save to
res://PlanetSetups/<id>.tres via ResourceSaver - reachable now from
the main menu's Map Editor button. The mutation methods
(new_setup/load_setup/add_*/move_*/remove_*/save_setup) are the whole
model and work standalone, which is what lets
tests/test_setup_editor.gd drive placement/move/delete/save/reload
directly without simulating touch input.

Every cross-file reference is a preloaded script const rather than a
bare global class name, matching #42/#44's load-order notes - this
file is instantiated from other scripts' function bodies, never as a
script-mode test entry point itself, so it wasn't at risk the same way
the test files were, but the pattern is kept consistent throughout.

Part of the #40 conquest-mechanic breakdown. This closes out the
breakdown: #41-#48 are now all implemented, tested, and merged.
2026-08-15 21:24:28 +02:00
Loïc Wisniewski
def224667d Add artillery enemy with telegraphed AOE (#46)
EnemyArtilleryComponent is a stationary siege piece (slow drift toward
the fleet, deliberately no pursuit component) that fires at the fleet
centroid on a long cooldown via a standalone ArtilleryShell -
telegraph ring (~1.8s, growing radius plus a filling countdown arc) ->
slow visible travel -> AOE explosion via Ship.take_damage() on every
player_ships node inside, same all-or-nothing shield absorption as
every other damage source. The shell is independent of the enemy that
fired it (added to the tree root, like EnemyProjectile/Projectile) so
the shooter dying mid-flight doesn't cancel an already-telegraphed
shot, and the impact point is fixed at fire time - never re-aimed at
where the fleet moves to afterward.

Part of the #40 conquest-mechanic breakdown.
2026-08-15 21:23:43 +02:00
Loïc Wisniewski
fc2b5ca74a Add standoff sniper enemy (#45)
EnemyStandoffComponent holds a preferred distance from the nearest
player ship instead of ramming (advance beyond it, retreat inside
retreat_range_factor of it, drift laterally in the comfortable band
between) - never paired with a pursuit component. EnemyRangedWeaponComponent
mirrors VesselWeaponComponent for an enemy: fires an EnemyProjectile
aimed at the target's CURRENT position rather than led, so a moving
ship can dodge it (projectile_speed 260 vs the interceptor's 460
max_speed). Registered in EnemyDefinitions so it's placeable from a
planet setup by enemy_id.

test_sniper_enemy.gd went through a few iterations to get its own
timing right: physics ticks can run ahead of a SceneTree test's own
_process() clock under this device's load, so the cooldown-violation
check reads the component's own fire_timer directly (a rising value
means it refired) instead of comparing projectile counts against a
wall-clock wait, and manually repositioned ships need their
move_target re-pinned or Ship's own steering silently drives them back
to the stale target.

Part of the #40 conquest-mechanic breakdown.
2026-08-15 21:23:01 +02:00
Loïc Wisniewski
c4e2ee7178 Add capture points and the planet conquest loop (#43)
CapturePoint tracks neutral/blocked/capturing/captured state by polling
proximity each frame (a ship in capture_radius, no enemy within
enemy_clear_radius), draining gradually rather than resetting when
interrupted, and calling GameState.mark_point_captured() on completion.
PlanetSetupHost now instantiates the real CapturePoint scene per
authored point (restoring already-captured state immediately on
re-landing) and spawns each setup's authored enemies, replacing the
placeholder markers from #42. CaptureLabel is a small self-polling HUD
(same convention as ResourceCounter) showing "CAPTURED n/m" or
"CONQUERED" with a one-shot fade on completion, themed per surface
profile alongside the existing bank/fleet captions.

test_planet_atmosphere_transition.gd's "no enemies on a landed
surface" assumption no longer holds now that setups can place guard
enemies by design - narrowed the check to what it actually needs to
prove: no space-side EnemySpawner/AsteroidField node leaks across the
scene swap.

Part of the #40 conquest-mechanic breakdown.
2026-08-15 21:22:10 +02:00
Loïc Wisniewski
65fe19aeb6 Backfill missing script UID sidecar files
Unrelated pre-existing gap: 11 scripts never got a committed .uid,
exposed by running a full --headless --import pass while working on
#40's sub-tickets. Godot generates these deterministically from the
script path, so this is a no-op for behavior.
2026-08-15 14:44:02 +02:00
Loïc Wisniewski
a1fb239a50 Add a main menu scene (#47)
MainMenu.tscn is now project.godot's entry point: Play / Map Editor
(disabled, explains itself - #48 wires it up) / Quit (hidden in web
builds), styled like the rest of the game's menus and reusing the
starfield shader for its background.

Play calls GameState.reset_new_game(), so a second playthrough in the
same process starts fresh instead of inheriting whatever the previous
run left in the autoload - existing tests still load Main.tscn
directly and are unaffected.

Part of the #40 conquest-mechanic breakdown.
2026-08-15 14:43:46 +02:00
Loïc Wisniewski
f14ee656ea Add enemy foundation: definitions, components, ranged damage (#44)
Enemy.gd now resolves stats/hull/color from EnemyDefinition resources
and composes behaviour from capability components (EnemyPursuitComponent
holds today's chase-the-nearest-ship logic), the same shape Ship.gd
already uses for vessels. Fleet lookups go through a "player_fleet"
group instead of a hardcoded ../Fleet sibling path, so an enemy parented
anywhere (e.g. under a planet surface's SurfaceFeatures) still finds it.

Also closes a real gap: enemies previously had no way to damage the
fleet at range at all (Ship only reacted to physical contact,
Projectile.gd was player-only). Ship.take_damage() is now a proper
entry point - contact damage routes through it unchanged - and
EnemyProjectile lets an enemy component fire at player_ships. Both
ranged enemy archetypes in #45/#46 build on this.

Part of the #40 conquest-mechanic breakdown.
2026-08-15 14:43:29 +02:00
Loïc Wisniewski
5326852807 Add planet setups as authored resources (#42)
PlanetSetup/PlanetSetupSpawn define a curated capture-point/enemy
layout for a surface profile; PlanetSetups scans res://PlanetSetups/
at first use (not a hardcoded preload table) so dropping in a new
.tres is the only step needed to add one, and resolves the same
setup for the same planet deterministically, same pattern as
WorldGen.planet_data_at_chunk. PlanetSurfaceController composes a
PlanetSetupHost onto the landed surface alongside the existing
resource-provider feature scene.

Ships two starter setups (rocky_default, gas_default) with
placeholder capture-point markers; #43 replaces the markers with the
real CapturePoint scene through the same apply_setup() entry point.

Part of the #40 conquest-mechanic breakdown.
2026-08-15 14:41:37 +02:00
Loïc Wisniewski
a1ad6cc844 Add planet conquest state and gate extractor placement (#41)
Planets now need every capture point held before an extractor can go
up there: GameState tracks per-planet capture progress keyed by the
same stable planet_id as extractors, and BuildController's four
extractor placement checks (menu enable, status text, info popup,
confirm) all re-check conquest, not just proximity/duplication.

Part of the #40 conquest-mechanic breakdown.
2026-08-15 14:41:19 +02:00
Loïc Wisniewski
7da4531f25 Fill bomber hull during cooldown 2026-08-15 10:41:24 +02:00
Loïc Wisniewski
f5c60f8805 Prioritize queued logistics deliveries 2026-08-15 10:41:18 +02:00
Loïc Wisniewski
eb79efdafe Track gas bomb cooldowns per bomber 2026-08-15 10:12:33 +02:00
Loïc Wisniewski
1d9afcbc2e Add gas bomber skill-shot vessel 2026-08-15 04:45:15 +02:00
Loïc Wisniewski
d0a9f5b971 Provision transporters with hubs 2026-08-15 00:09:10 +02:00
Loïc Wisniewski
663648d949 Refine context actions and preserve fleet landings 2026-08-14 23:15:12 +02:00
Loïc Wisniewski
18a7d44db4 Make shield vessels unarmed hexagons 2026-08-14 22:46:30 +02:00
Loïc Wisniewski
3fc04f816a Supply lab research with transporters 2026-08-14 22:46:25 +02:00
Loïc Wisniewski
20ba0011fa Slow logistics vessel travel 2026-08-14 22:30:19 +02:00
Loïc Wisniewski
db0e6a0ec2 Add manual extractor resource transfers 2026-08-14 22:21:01 +02:00
Loïc Wisniewski
e2d4297ee5 Key extractors by stable planet identity 2026-08-14 22:16:40 +02:00
Loïc Wisniewski
14081ad230 Compose vessels from definitions and capabilities 2026-08-14 22:12:57 +02:00
Loïc Wisniewski
0504b24d8d Compose buildings from definitions and capabilities 2026-08-14 21:44:28 +02:00
Loïc Wisniewski
1721edd123 Store resources in generic inventories 2026-08-14 20:32:58 +02:00
Loïc Wisniewski
02643c76c4 Drive planets from definitions 2026-08-14 16:13:24 +02:00
Loïc Wisniewski
2f68515f33 Compose planet surfaces from profiles 2026-08-14 16:05:32 +02:00
Loïc Wisniewski
e4df9bc41a Add transfer amounts and active gas vents 2026-08-14 13:44:10 +02:00
Loïc Wisniewski
31ebfa905e Guarantee a nearby gas giant 2026-08-14 12:07:50 +02:00
Loïc Wisniewski
8231e165ff Refactor multi-resource UI and transfers 2026-08-14 11:54:31 +02:00
Loïc Wisniewski
ac5b5415f5 Correct gas HUD and transfer rows 2026-08-13 23:25:46 +02:00
Loïc Wisniewski
cccdc85c9e Show gas icons in resource displays 2026-08-13 22:33:12 +02:00
Loïc Wisniewski
4c82155cf3 Support gas cargo transfers 2026-08-13 22:17:24 +02:00
Loïc Wisniewski
e9acca569e Queue lab research from local stock 2026-08-13 22:04:21 +02:00
Loïc Wisniewski
798deb48a1 Animate ship crafter deliveries 2026-08-13 21:39:05 +02:00
Loïc Wisniewski
fc68c4e8cf Add gas giant resource system 2026-08-13 21:07:35 +02:00
Loïc Wisniewski
8f7b58f2a8 Highlight selected ship lab cards 2026-08-13 20:56:24 +02:00
Loïc Wisniewski
d20a62a26f Expand contextual actions horizontally 2026-08-13 20:54:22 +02:00
Loïc Wisniewski
d79c756d5b Collapse contextual building actions 2026-08-13 20:49:15 +02:00
Loïc Wisniewski
8219893cda Improve ship lab upgrade selection 2026-08-13 16:45:08 +02:00
Loïc Wisniewski
5755568346 Route extractor ore through transporters 2026-08-13 16:38:41 +02:00
Loïc Wisniewski
13492a52e2 Make research tree vertically scrollable 2026-08-13 16:25:41 +02:00
Loïc Wisniewski
06081c41b0 Improve ship crafter and hangar controls 2026-08-12 23:08:43 +02:00
Loïc Wisniewski
82552464d2 Add timed craft queue and hangar 2026-08-12 22:47:46 +02:00
Loïc Wisniewski
b6427b080d Remove the one-way ore API superseded by the transfer menu
The loose end from #24. withdraw_to_fleet, unload_into_building and
deliver_cargo were the old one-way moves behind "Load ore" and "Store
ore here"; every one of them is a special case of transfer_to_building /
transfer_to_fleet, and none had a caller left outside tests. The
cargo_delivered signal had no listeners at all.

Not just tidying: unload_into_building routed through
deposit_to_building, which does NOT special-case the mothership - it
writes b.stock, while building_holdings() returns `rocks` for a
mothership and ignores stock entirely. So unload_into_building(
mothership_id) would have parked ore in a field nothing reads, with no
error. Nothing called it that way, but it is a plausibly-named function
sitting there to be reached for later. transfer_to_building handles it.

CARGO_LOAD_BATCH stays - the transfer menu's batch buttons use it; only
its role as withdraw_to_fleet's default argument is gone.

The #24 note expected folding these in to be costly for
test_cargo_readout, which places no buildings. It is not: the transfer
primitives work off GameState records rather than spawned nodes, so a
single add_building() in setup is enough and it stays a display test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:07:52 +02:00
Loïc Wisniewski
fe1c721d64 Restore the crafter binding lost in an over-wide edit
This is the actual cause of "the build a ship menu buttons do nothing",
reported four times. Opening the crafter panel stopped recording WHICH
crafter: when the auto-unload was replaced with an explicit store button,
the replaced block reached one line further up than intended and took
`craft_building_id = current_building.building_id` with it. Every handler
in the panel then bailed out at its `if craft_building_id == 0` guard,
while the panel itself still opened and Close still worked - because
Close is the only button that does not need the binding.

The silent `return` on that guard is what let it pass unnoticed, so it
now says so instead.

The test was complicit: it set craft_building_id by hand, which skipped
the exact step that was broken. It now parks the fleet on the crafter,
waits for the action button to debounce in, and presses it - the way the
game does. Verified against the broken code: it fails with
"opening the panel did not record which crafter".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:55:12 +02:00