Commit graph spaceship/PlanetSurface.tscn
Author SHA1 Message Date
Loïc Wisniewski
0fb8f9300b Twin-stick combat: aim and fire with a second stick, MOBA-style ability ring
The fleet's guns picked their own targets and fired on their own, so combat
happened whether or not the player did anything - there was nothing to aim and
nothing to decide, and the right half of the screen held nothing at all.
Firing is now an action. VesselWeaponComponent no longer seeks a target; it
shoots along a heading, and the only thing that supplies one is the new
right-hand stick via FireStickController. A fleet arriving from another scene
starts with its guns silent, which is the correct resting state.

A subtle dashed guide runs from the fleet along the aimed heading while the
stick is held, out to the guns' OWN range - read from a weapon that is
actually aboard, so the line can never promise reach the fleet does not have.

Layout: the two sticks take the bottom corners, the pivot pair is centred
between them at the bottom edge, and the laser and gas bomb sit on an arc up
and left of the fire stick where a thumb already is. Those four are now round
- IconButton grew a round_frame mode, and the in-world action controls use it
while panel and menu buttons stay square, so the two kinds of button read as
different kinds of thing.

Worth recording, because it cost a screenshot to find: a script-declared
property set BEFORE the `script = ExtResource(...)` line in a .tscn is
silently dropped. Godot's parser has no idea the property exists yet. The
round frames were all correctly set in both scenes and none of them applied.

Also fixed researtch_lab_garrison.tres, which was authored under the void
profile - that is the black hole surface, so a lab garrison was spawning on
black holes.

Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds
the real stick toward the enemy rather than poking weapon components, so it
exercises the path the player uses. test_fire_stick is new and covers the
change itself: an idle stick leaves a target parked well inside range
untouched, which is precisely what the old gun would never have done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:29:21 +02:00
Loïc Wisniewski
f5a16a7b95 Share one backdrop between game and editor, and fix three input/economy bugs
The setup editor and the landed surface drew different backgrounds for the
same planet. Each scene carried its own hand-written ShaderMaterial, and only
the game side ever swapped in a profile's shader, so a gas giant authored in
the editor showed recoloured craters while the game showed cloud bands. Both
now instance one SurfaceBackground.tscn and configure it through one method,
and every profile names its own shader - the old "null means keep whatever
the scene already had" convention was the drift, since each scene "already
had" something different. The shared material is resource_local_to_scene, or
two live surfaces would repaint each other.

The gas giant's bands broke into disjoint segments once per cell: the second
turbulence octave doubled only the INTEGER half of a lattice coordinate,
which is a different point in every cell rather than the same point at twice
the frequency. Measured, the old form steps 0.153 at a cell boundary against
0.0002 now. A second discontinuity went with it - the camera cell index
arrives pre-wrapped, so hashing it raw rearranged the whole sky every 32
cells; hashes are now wrapped per octave and the field is exactly periodic.
Bands also gained low-frequency octaves and per-row differential drift.

WorldMap swallowed every touch in _input(), which runs before the GUI system,
so CloseButton never saw one: the map could be opened and not closed. Android
and desktop hid it behind Godot's emulated mouse event, which this never
swallowed; a phone browser gives no such second chance. Its test had been
closing the map by calling the method the button was failing to reach.

Construction now draws on the fleet hold as well as the bank. With an empty
starting bank and ore bankable only at a mothership, charging construction to
the bank alone made the first mothership cost ore that could not yet exist.

Also: a conquered planet no longer re-spawns its garrison (guarded on having
capture points, since a setup with none registers as vacuously conquered);
planets keep the map they drew, with the candidate list sorted rather than
left in filesystem order and assignments pruned rather than cleared on
rescan; and the map editor lists setups at a tappable size and can delete
one, as an explicit armed mode rather than a tap away from destroying work.

Full suite 106/106.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:16:38 +02:00
Loïc Wisniewski
657ca9d035 Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68)
The crater shader ran on every pixel of a fullscreen rect every frame and
was by far the most expensive thing in the surface scene. Nothing else came
close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid
per-frame queue_redraw() for transform-only changes, and LunarBackground.gd
is a handful of scalar ops.

Three reductions to the field itself:

- 5x5 -> 3x3 neighbour scan. That width was never the geometric answer;
  it was insurance against craters seen clipping at cell boundaries
  on-device, i.e. a crater reaching past its nominal radius because a hash
  returned a value outside [0,1] and extrapolated mix(). Clamping the
  crater CENTRE into its own cell, alongside the radius clamp already
  there, bounds the reach whatever the hash returns - so a crater two cells
  out is >= 1.0 cells away, a normalized distance of >= 2.63, past every
  threshold the shading reacts to (the last saturates at 1.15). The outer
  ring provably cannot change a pixel. This removes the clipping bug's
  cause instead of hiding it behind 16 extra samples.
- One hash42 per cell instead of hash21 twice plus hash22, decorrelating
  on output lanes rather than input offsets.
- Squared distances in the loop, square-rooted twice at the end rather
  than once per sample.

Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms
fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction.

Then the larger change: the field is a fixed function of world position, so
every frame recomputed an identical answer. It is now made periodic (the
cell index is wrapped before hashing, which makes a Worley field tile
exactly rather than merely blend), baked once at load into one tile, and
scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch
per pixel. Two honest costs: the field repeats every 7040 world units, and
a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is
forgiving of both, having no landmarks and being smooth gradients already.

The bake waits a frame and asks the material whether it actually samples a
field_texture, so a gas giant - which swaps in its own background shader
(#51) - does not pay for a 2048-square tile it will never read. Headless
runs skip it entirely, having nothing to rasterize.

tests/test_lunar_field.gd guards the reasoning, since a fragment shader
cannot be asserted on directly: the premises are still in the source, the
worst-case ring-2 geometry stays clear of every threshold independent of
the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points,
the field repeats exactly in both axes, and the miss sentinel still cannot
overflow mediump into +Inf and then NaN.

test_surface_profiles.gd asserted "a rocky landing leaves the background
alone" by naming lunar_surface.gdshader; it now reads the shader from the
scene, which is the property it actually cares about.

PlanetSurface.tscn's edit also carries the planet-name relocation described
in the HUD commit - same scene file, so the two cannot be split.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
Loïc Wisniewski
42535c7320 Fix UI overlaps: SpeedFader vs status labels, packed ship lab layout (#67, #71)
SpeedFader was missing anchor_top/anchor_bottom = 1.0 in both Main.tscn
and PlanetSurface.tscn, so it rendered TOP-anchored at a fixed y while
its siblings (LaserStatus, GasBombStatus) are bottom-anchored - on a
720x1280 viewport both landed in the same screen region, directly on
top of each other. Properly bottom-anchored SpeedFader now, positioned
above both status labels instead.

Separately, the ship lab's 7 vessel-name labels (#57-#59 grew the
roster from 4 to 7 without widening their row) were laid out 120px wide
on only a 90px pitch - a fixed 30px overlap between adjacent labels
regardless of text length, worse for genuinely long names like
"Interceptor"/"Transporter". Narrowed each label to fit its own lane
with real gaps, dropped the font size, and enabled word-wrap; shifted
the upgrade-card rows below down to give the now-taller label row room.
2026-08-17 19:47:09 +02:00
Loïc Wisniewski
facbd83dd2 Laser shooters consume energy from the fleet hold when firing (#61)
Mirrors #60's gas bomber ammunition exactly: ENERGY_PER_SHOT (8) drawn
from GameState.fleet_inventory before consuming the firing ship's
cooldown, the button's enabled rule requires affordability too, and an
unaffordable-but-otherwise-ready press still explains itself through a
new LaserStatus label rather than silently doing nothing.

Added two small generic GameState helpers this needed:
deposit_fleet_resource()/deposit_bank_resource(), counterparts to
#60's withdraw_fleet_resource() for resources (energy) with no
dedicated fleet_cargo/fleet_gas_cargo-style property. Along the way,
confirmed deposit_resource_to_building() on a "bank" building deposits
into that building's own per-record inventory, NOT bank_inventory
itself - only building_resource()/building_inventory() special-case
the "bank" capability to redirect there.

test_laser_vessel.gd (#58) updated to stock fleet energy first, since
shots are no longer free - same fix #60 required for the gas bomber's
own pre-existing tests. Also fixed test_laser_vessel.gd's own instance
of the bare-ClassName.CONST script-corruption trap (documented in
#60's commit): var skill: LaserController and a bare
LaserController.BEAM_HALF_WIDTH reference, mirrored into a local const
instead.
2026-08-16 19:11:31 +02:00
Loïc Wisniewski
17d0cf7e66 Gas bombers consume gas from the fleet hold when firing (#60)
Shots cost GAS_PER_SHOT (5) from GameState.fleet_inventory, not the
bank - the fleet has to be carrying its own ammunition. The button's
enabled rule now also requires affordability, and a press on a ready-
but-unaffordable bomber still consumes the touch to explain itself via
a new status label (GasBombStatus, IconButton's own "greyed but still
pressable" convention) rather than either silently doing nothing or
consuming the bomber's cooldown for a shot that never fires.

Also documents a real, reproducible GDScript trap hit while writing
tests/test_gas_bomber_ammo.gd: a bare `ClassName.SOME_CONST` reference
anywhere in a -s entry test script can corrupt method resolution for
the WHOLE script, making an unrelated, earlier `some_node._process()`
call fail with "Nonexistent function '_process' in base 'Nil'" despite
the node being valid and the method existing (confirmed via
has_method()). Root cause unconfirmed; fixed by mirroring the constant
's value into a local const instead of referencing the class directly,
in all three gas-bomber tests.

tests/test_gas_bomber.gd and test_gas_bomber_cooldowns.gd updated to
stock fleet gas first, since shots are no longer free.
2026-08-16 18:54:44 +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
ed46cd77e6 Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.

LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.

The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.

Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +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
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
1d9afcbc2e Add gas bomber skill-shot vessel 2026-08-15 04:45:15 +02:00
Loïc Wisniewski
2f68515f33 Compose planet surfaces from profiles 2026-08-14 16:05:32 +02:00
Loïc Wisniewski
8231e165ff Refactor multi-resource UI and transfers 2026-08-14 11:54:31 +02:00
Loïc Wisniewski
e9acca569e Queue lab research from local stock 2026-08-13 22:04:21 +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