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.
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.
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.
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.
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.
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.
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.
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.
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>
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>
The bug reported three times was in the input path, and every test I had
called the handlers directly - so they all passed while the button was
inert. test_crafter_feedback now taps the control the way a touch does,
including after the build menu has been opened and closed, which is the
sequence that used to block every button in the game.
Verified against the old guard: the direct-call assertions still pass,
and the real tap fails with "store button dead after the build menu was
opened and closed".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The real cause of "the crafter buttons do nothing", reported three
times. IconButton._blocked_by_modal tested `menu.visible`, but hiding a
parent leaves a child's own `visible` flag true - and the build menu's
info popup is itself one of the modal panels. So opening any building's
info popup and then closing the build menu with the X left an invisible
popup still claiming to be open, and from that moment EVERY button in
the game was blocked, with no way for a player to get out of it.
It tests is_visible_in_tree() now, and closing the build menu also
closes its popup rather than leaving it half-open.
My earlier attempts kept missing it because the probes called the
handlers directly - they exercised the crafter logic, which was fine all
along, and never the input path in that state. The regression test
reproduces the exact sequence: open the build menu, show a building's
info, hide the menu, then check an unrelated button still fires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>