Commit graph spaceship/Fleet.gd
Author SHA1 Message Date
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
Loïc Wisniewski
7817d1ffc6 Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
DISTORDER STACKING. Each space-time distorder vessel past the first raises
the fleet's speed ceiling by 1.0, so a second one is worth crewing instead of
being dead weight. GameState gains max_fleet_speed_multiplier, pushed by
Fleet whenever composition changes - the same direction as every other
fleet-derived figure, since GameState does not reach into the scene tree to
count ships. The energy drain is normalised against the CURRENT ceiling, so
full boost costs the same per second however wide it is: extra vessels buy
more speed, not cheaper speed. Losing one re-clamps an over-ceiling boost
rather than leaving the fleet travelling faster than its crew can sustain.

TRANSFER MENU. Rows overlapped, and the last one ran into the hint text.
row_height is divided from the available space, but each row's contents used
a fixed pixel ladder - glyph at +10, buttons at +88, selector at +142 - which
needs about 244px. Three resources fit; the fourth cut rows to 180 and every
one spilled 64px into the next. Every offset is now a fraction of the row, so
it cannot overflow. Also dropped the per-row AMOUNT caption (four copies of a
word the footer hint already explains) and moved each glyph and name to the
TOP of its row, where they read as its heading rather than the next row's.

HANGAR. Six vessel types in one row gave each about 100px for a 155px label,
so the names ran together. Now a 3x2 grid per section, sized from the space
between the first heading and the status line, using the empty lower half.

LAB RESEARCH REWARD. A harder enemy lab is worth more research points, and
how hard a lab is depends on the layout authored for it - how many capture
points, how many defenders - which the planet TYPE cannot know. PlanetSetup
carries research_point_reward, set in the map editor, overriding the planet
type's flat rate. 0 means "no opinion", so every map authored before this
keeps its old value.

RESEARCH LAB BACKGROUND. Its own shader instead of the crater field with a
red tint, which read as "rocky planet, tinted" rather than someone's
facility: plated floor, seams, service conduits, lit junctions. Loop-free by
construction (#68's lesson applied up front) - conduits stay continuous
across cells by keying a horizontal run to cell.y and a vertical to cell.x,
so neighbouring cells agree without any cell scanning its neighbours.

ENEMY DETAIL. Every enemy was a single flat outline, which left the artillery
and the turret spawner as the same octagon at two sizes - the silhouette
carried no information about what the thing did. EnemyDefinition gains
detail_lines and detail_rings as DATA, so adding an enemy still means adding
a definition rather than editing Enemy.gd, and the four non-default hulls are
redrawn: a long-barrelled sniper, a squat mortar, a hive with launch bays, a
capital ship with spine and ribs.

Three things drew an enemy from its definition - the enemy, the map editor's
markers, and the palette buttons - each reimplementing it from hull_points
alone, so the new detail appeared in the game and nowhere else and an editor
marker stopped matching what it placed. They now share
EnemyDefinitions.draw_definition().

ARTILLERY. The 1.8s charge-up before the tower fired arrived long before the
threat was legible and mostly read as the tower being stuck. It fires
immediately now; the warning is the shell you can see in the air plus a small
cross on the ground at the impact point. Deliberately not a blast-radius ring
- a large translucent area was easy to read as the explosion having already
happened, where the cross says "move off this spot".

MAIN MENU. No Quit in any build (a web build has nothing to quit to, and
Android's back gesture already does it), a plain black background, and the
game is "Spaceship" - in the title and in config/name, which is what drives
the browser tab for the web export. change_scene_to_file() returns an error
and otherwise fails silently, leaving the menu up, which is indistinguishable
from an unpressed button; it is now checked and reported on screen.

Full suite green at 99/99.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:54:46 +02:00
Loïc Wisniewski
67cb853397 Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.

Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.

The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:

- fleet_velocity is now fed FORWARD into the steering instead of carried
  alongside it. The ship aims to travel at its slot's velocity plus a
  correction closing the gap, so holding formation costs no error and
  `velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
  case where converging asymptotically would leave a residue. While the
  slot moves the ship steers continuously - a snap there was the teleport
  that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
  request, so no sequence of commands can leave a vessel over its own top
  speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.

tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.

test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
Loïc Wisniewski
d68f4d4f88 Refactor recent gameplay systems after code review 2026-08-16 21:38:07 +02:00
Loïc Wisniewski
116e0f97d3 Add space-time distorder vessel and fleet speed fader (#59)
GameState.fleet_speed_multiplier (1.0-3.0) is applied at the point of
use - Fleet's cruise anchor and Ship's own max_speed/acceleration -
rather than baked into stored stats, so a stat upgrade researched
mid-boost can't corrupt the base value and the fader takes effect
immediately with no re-apply step.

Ship.arrive_radius is scaled by the same multiplier: at higher speeds
each physics tick covers a proportionally larger step, and once that
step outgrows a fixed arrive_radius the ship can overshoot its
snap-to-target zone every tick while chasing a target that's now also
receding faster - visible as the fleet stretching apart under boost.
Scaling it preserves the same "snaps onto its slot" tightness at any
boost level that 1.0x already has.

SpeedFader.gd is a self-contained vertical drag control (TouchJoystick's
raw-touch idiom, not _gui_input) that writes straight to the autoload
rather than reporting a value for something else to apply. It also
resets the multiplier to 1.0 the instant fleet.has_capability("distorder")
goes false, so a fleet can never be left permanently boosted by a vessel
it no longer has.

VesselDistorderComponent carries no per-shot behaviour (energy drain
while boosted is #62) - its presence is what matters. Registered in
VesselDefinitions, wired into the crafter/hangar/ship-lab menus per
#57's note (5/5/6 -> 6/6/7 columns).
2026-08-16 17:59:04 +02:00
Loïc Wisniewski
f040327523 Cap fleet cargo capacity by composition (#56)
The fleet hold was unbounded - GameState.fleet_inventory had no
total_capacity, so a lone ship could carry unlimited resources. Capacity
is now data-driven: only vessels declaring the "cargo" capability
contribute their cargo_capacity stat (interceptor: 20; shield vessel and
gas bomber: 0, unarmed transporters are never in the fleet at all).
Fleet.gd recomputes it in the one place composition can change
(spawn_ships/add_ship/store_ship/_on_ship_destroyed), which also covers
the space<->surface scene-swap restore since that goes through
spawn_ships().

Rock.gd and GasVentField.gd (the two "fly over it to collect" pickup
paths) now check what collect_cargo()/collect_gas_cargo() actually
accepted before consuming their source, instead of assuming an
unlimited hold. The HUD's FLEET HOLD caption shows the live total/cap.

Also fixes test_resource_transfer.gd, which pushed more ore through a
2-ship fleet than the new cap allows - it now spawns the largest fleet
every unlocked doctrine permits, since that test is about the transfer
UI rather than the cap itself (test_fleet_cargo_limit.gd owns that).
2026-08-16 14:53:26 +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
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
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
19ac758da1 Typed owed vessels, ship lab building type, shield vessel gate
Groundwork for #5. vessels_owed carries the VESSEL TYPE rather than a
bare count, so a shield vessel finishing while the player is on a planet
comes back as a shield vessel. Fleet.add_ship takes a type, and vessels
already flying are re-statted when an upgrade is researched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:10:07 +02:00
Loïc Wisniewski
0e071877e2 Fix the stop stutter, and hide unresearched buildings from the menu
Forgejo #13: releasing the stick paused, then moved again before
settling. stop() aims each ship at a braking point ahead of it, but the
ship's own velocity was ~0 - the anchor had been carrying it - so it
started from REST, accelerated into its own stopping distance, and
halted a second time. Measured: 0 -> 45 -> 90 -> 135 -> 180 -> brake.
stop() now hands the fleet's motion to the ship, so the release is one
continuous decelerating coast (222 -> 0) to the identical endpoint.
test_joystick_control now asserts the speed one frame after release is a
real fraction of cruise, and never rises afterwards.

Forgejo #21: unresearched structures are no longer listed greyed out.
They are hidden, and the menu repacks around what is left - a fixed grid
with entries hidden would otherwise leave holes where they used to be.
An entry alone on its row is centred rather than hanging in the left
column. Researched-but-not-usable is still shown greyed: an extractor
with no planet under the fleet is "not now", not "not yet".

This reverses a documented decision (the old comment argued that always
listing everything stops the player wondering whether the game has it at
all), so test_skill_tree's matching assertion was updated from
greyed-but-listed to hidden. The gate itself is unchanged and still
refuses a build forced past the menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:38:17 +02:00
Loïc Wisniewski
3573ccf44f Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.

Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.

Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.

The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.

Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
Loïc Wisniewski
b641fa5891 Skill tree, node-based building animation, bigger UI, and a 19x faster test suite
Gameplay and UI
- Research tree at the mothership gates which structures can be built and
  grants permanent fleet-capacity upgrades (GameState.SKILL_TREE,
  SkillTreeView). Node depth is derived from prerequisites, so the layout
  cannot drift out of step with the dependencies.
- Building info popup before committing to a build, with per-building cost
  and description.
- Ship crafter redrawn as a space factory with an assembly animation;
  mothership redrawn as a radial flagship; command centre as a plotting hub.
- Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build.
- UI and font scaled up for phone use, via a custom Theme resource -
  gui/theme/default_font_size does not reach the default theme.
- Asteroids no longer spawn on top of the fleet's start position.
- Fleet formation origin derived from the layout's own offsets rather than
  the centroid, fixing the lurch when a lopsided formation started moving.
- Fleet.stop() brakes from the ship's TRUE motion (velocity +
  fleet_velocity). Ship zeroes its velocity when seated in its slot even
  while the anchor drags that slot forward, so braking from velocity alone
  stopped the fleet dead instead of coasting - and whether it did depended
  on the frame rate.

Animation architecture (issue #10)
- Buildings, projectiles and explosion VFX no longer animate by redrawing.
  Static geometry is rasterised once in _draw(); moving parts are NeonPiece
  children animated through node properties, which reuse the cached
  geometry. Every NeonDraw call expands into four antialiased polylines, so
  redrawing to move something re-tessellated all of it.

Test suite (issue #8): 1075s -> ~55s
- Runs from a copy on Termux's own filesystem. The project lives on
  Android's FUSE bridge, read through proot, where load("res://Main.tscn")
  alone cost 8.5s per test against ~2s natively - and nearly every test
  loads it.
- Runs 6 tests at a time, in a single proot-distro login, with output
  buffered per test and printed only on failure.
- --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's
  result no longer depends on how busy the machine is. Without it,
  parallelism made two tests fail on 2 of 3 runs.
- A test passes only if it both exits 0 and prints ": PASS".
- 38 tests, green on three consecutive runs.

Bundles Carrois Gothic SC (Apache 2.0) with its licence text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
Loïc Wisniewski
85814ce88c Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).

All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.

Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.

Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00