Commit graph spaceship/world
Author SHA1 Message Date
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
bd0cc842ff #105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and
goes back to circling. It steers by writing move_target and lets Ship's own
steering fly it, so it accelerates, banks and brakes exactly like a vessel
in the formation.

It is not in Fleet.ships. It does not follow the formation, does not count
against max_fleet_size(), and does not move when the player flies away -
which is the whole point of stationing it. A garrison that ate a fleet slot
would mean stationing a vessel cost the player a place in their own wedge.

The leash is the load-bearing part. Enemies are judged from the POST rather
than from the vessel, so a garrison that has drifted out to its leash cannot
see further still and be walked away from its outpost one enemy at a time,
and an engagement is clamped to a circle around the post rather than to a
straight line, so it still manoeuvres while pinned.

The orbit starts from where the vessel actually is rather than a random
bearing. A random one makes the first move_target a point on the far side of
the circle, and the vessel flies straight across the middle - through the
outpost it is guarding.

EnemyTargets.nearest_ship now searches the player_ships GROUP instead of
Fleet.ships: an enemy that ignored the guns shooting at it, because they
were not in the player's formation, would let a garrison hold a position
without ever being fought back.

The laser vessel is deliberately not garrisonable. Firing the laser lives in
LaserController, a fleet ability driven by the laser button, rather than in
VesselLaserComponent - so a garrisoned laser would hold its position and
never shoot, and offering a vessel that cannot fight is worse than a short
list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:47 +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
26f3728546 #100 (#97b): waves replace the roaming enemy spawner
The old spawner put one enemy on the map every 3-6 seconds for ever. There
was no rhythm to the danger and nothing to prepare for. Enemies now arrive
in waves: a long quiet, a warning, an attack, and quiet again once the wave
is dead.

The clock lives in GameState, because Main.tscn is destroyed on every trip
to a planet surface and a wave must not be cancelled, nor restarted from
zero, by a landing. The spawning does not: GameState cannot see the world,
so EnemySpawner reads the phase and puts the enemies in it. That also
settles what happens when a wave turns while the player is on a surface -
the wave is waiting in space when they come back.

EnemySpawner keeps its name deliberately. It is still the thing that puts
enemies in the world, and 88 test files already say
`get_node("EnemySpawner").set_process(false)` to ask for a quiet map.

Strength is a function of the FLEET, per the ticket, with the wave number as
a slower second term so a player who never grows still meets escalation. The
budget is spent most-expensive-first, so a large wave is heavier rather than
merely longer, and min_strength keeps the first waves to chasers whatever
the arithmetic says.

"No timer should be visible" is enforced rather than merely observed:
GameState offers wave_warning_pulse(), a 0..1 throb, and no
remaining-seconds accessor at all, so the alert cannot drift into being a
countdown. The test pins both - that no such accessor exists, and that the
alert's own text contains no digits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:37:42 +02:00
Loïc Wisniewski
5f1b5a9b29 #95: stop printing the bank twice in the research menu
The research table's LOCAL column was not merely equal to BANKED, it was the
same store: research lives on the mothership, the mothership is also the
bank, and building_inventory() redirects any bank-capable building to
bank_inventory itself. ResourceBar now drops LOCAL wherever it resolves to
the bank and lets the remaining two columns share the width. Anywhere else -
a crafter, a lab, the transfer panel - the column is untouched.

The "Research" heading went with it. It named a panel you can only reach by
pressing the research button, and it cost the tree canvas 128px, which the
table and the tree have now taken back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 11:35:14 +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
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