Commit graph spaceship/controllers/BuildController.gd
Author SHA1 Message Date
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
080e3bb928 #147: one resource per line, and the price back under its button
Both halves of the report had the same cause: a craft cell laid out with
more slack than its row pitch had room for.

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

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

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

The test pins what the ticket describes rather than the numbers behind it: a
multi-resource price is stacked, its content stays inside its own box, it
sits within 80px of its button, and no craft entry overlaps another - the
price of one row into the button of the next included, which the old
geometry did by 14px whatever was priced.
2026-08-29 13:05:46 +02:00
Loïc Wisniewski
2206b55873 #145, #144, #143: a quest that could not be finished, and two boxes that overhung
#145 - the extractor quest could never be completed. Extractors are placed
onto a planet from space and live in GameState.extractors, with their own
position, stock and id; they are not in `buildings`. So the goal asked
has_building_of_type("extractor"), which is false however many the player
builds. Not the death the reporter guessed at - the quest was simply asking
a question that had no yes.

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

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

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

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

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

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

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

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

Two things cleaned up on the way:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:02:20 +02:00
Loïc Wisniewski
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
Renamed from BuildController.gd (Browse further)