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
|
|
|
extends Node2D
|
|
|
|
|
|
|
|
|
|
@export var min_zoom: float = 0.4
|
|
|
|
|
@export var max_zoom: float = 6.0
|
|
|
|
|
@export var camera_follow_speed: float = 5.0
|
|
|
|
|
# Distance (world units) below which the camera snaps exactly onto the fleet
|
|
|
|
|
# instead of continuing to lerp. Without this the exponential lerp only ever
|
|
|
|
|
# approaches the target asymptotically and NEVER arrives, so the camera keeps
|
|
|
|
|
# drifting by a fraction of a pixel every single frame - forever, even with
|
|
|
|
|
# the fleet completely stationary. That kept the background shaders' camera_pos
|
|
|
|
|
# uniform changing every frame, which made screen tearing permanently visible
|
|
|
|
|
# as horizontal bands of the background shifted sideways against each other.
|
|
|
|
|
# Same class of bug (and same fix) as the ship arrival oscillation.
|
|
|
|
|
@export var camera_snap_distance: float = 0.5
|
|
|
|
|
@export var debug_input_logging: bool = false
|
|
|
|
|
|
|
|
|
|
@onready var camera: Camera2D = $Camera2D
|
|
|
|
|
@onready var fleet: Node2D = $Fleet
|
|
|
|
|
# Optional: scenes without a joystick (none currently) still work.
|
|
|
|
|
@onready var joystick: TouchJoystick = get_node_or_null("UI/Joystick")
|
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
|
|
|
# The right-hand stick: aims and fires. Optional for the same reason as the
|
|
|
|
|
# movement stick - a scene without one still runs, with the guns silent.
|
|
|
|
|
@onready var fire_stick: TouchJoystick = get_node_or_null("UI/FireStick")
|
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
|
|
|
# Navigation section. Optional so a scene without it still works.
|
|
|
|
|
@onready var pivot_cw_button: IconButton = get_node_or_null("UI/PivotCWButton")
|
|
|
|
|
@onready var pivot_ccw_button: IconButton = get_node_or_null("UI/PivotCCWButton")
|
2026-08-15 04:45:15 +02:00
|
|
|
@onready var gas_bomb_controller: GasBombController = get_node_or_null("GasBombController")
|
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
|
|
|
# Untyped on purpose: LaserController resolves fine as a static type when a
|
|
|
|
|
# script IS the -s entry point, but the same class_name reference 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 - for a class_name this new. Node + duck-typed calls sidesteps it.
|
|
|
|
|
@onready var laser_controller: Node = get_node_or_null("LaserController")
|
2026-09-02 13:43:56 +02:00
|
|
|
@onready var dash_controller: Node = get_node_or_null("DashController")
|
2026-08-16 17:59:04 +02:00
|
|
|
@onready var speed_fader: Node = get_node_or_null("UI/SpeedFader")
|
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
|
|
|
|
|
|
|
|
var touches: Dictionary = {}
|
|
|
|
|
var pinch_active: bool = false
|
|
|
|
|
var pinch_start_distance: float = 0.0
|
|
|
|
|
var pinch_start_zoom: float = 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Held pivot buttons turn the formation continuously at this rate; a quick
|
|
|
|
|
# tap that accumulates less than PIVOT_MIN_STEP is topped up to it on
|
|
|
|
|
# release, so tapping still produces a clear, visible nudge.
|
|
|
|
|
const PIVOT_SPEED: float = 1.6
|
|
|
|
|
const PIVOT_MIN_STEP: float = PI / 10.0
|
2026-08-17 19:47:50 +02:00
|
|
|
# #72: the tap top-up below used to fire in full on EVERY release, with no
|
|
|
|
|
# regard for how recently the last one landed - so tapping fast enough
|
|
|
|
|
# granted rotation far above PIVOT_SPEED's smooth rate, well beyond what
|
|
|
|
|
# holding the button can ever produce. Each of those artificially-frequent
|
|
|
|
|
# re-seats (Fleet.apply_formation()) forces every ship into a fresh
|
|
|
|
|
# max-speed catch-up dash toward its new slot with no time to settle
|
|
|
|
|
# between them, and since outer-wedge ships swing further per pivot than
|
|
|
|
|
# inner ones, spamming made them visibly diverge in speed ("desync") while
|
|
|
|
|
# each individual ship's own bounded max_speed made the fleet as a whole
|
|
|
|
|
# read as constantly at its speed ceiling ("build up speed"). This cooldown
|
|
|
|
|
# is exactly the time a genuine hold needs to reach PIVOT_MIN_STEP on its
|
|
|
|
|
# own, so tap-spam can't out-pace holding the button down.
|
|
|
|
|
const PIVOT_TAP_COOLDOWN: float = PIVOT_MIN_STEP / PIVOT_SPEED
|
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
|
|
|
|
|
|
|
|
var pivot_hold_amount: float = 0.0
|
2026-08-17 19:47:50 +02:00
|
|
|
# Tracked PER DIRECTION, not shared - a single quick correction (tap the
|
|
|
|
|
# other way right after an overshoot) is normal play, not the spam this
|
|
|
|
|
# cooldown targets, and Q/E each independently producing their own minimum
|
|
|
|
|
# step back-to-back is an existing, tested guarantee (test_keyboard_input.gd).
|
|
|
|
|
# Only repeatedly re-tapping the SAME direction needs to be capped.
|
|
|
|
|
var pivot_cw_tap_cooldown_left: float = 0.0
|
|
|
|
|
var pivot_ccw_tap_cooldown_left: float = 0.0
|
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
|
|
|
# Touch index currently holding each pivot button, or -1. These buttons are
|
|
|
|
|
# driven straight from raw touch events rather than through _gui_input,
|
|
|
|
|
# because Godot's Control GUI treats touch as a single emulated pointer and
|
|
|
|
|
# will not reliably deliver a SECOND simultaneous finger to another
|
|
|
|
|
# Control - which meant pivoting while steering with the joystick silently
|
|
|
|
|
# did nothing. The joystick already bypasses the GUI for the same reason.
|
|
|
|
|
var pivot_cw_touch: int = -1
|
|
|
|
|
var pivot_ccw_touch: int = -1
|
|
|
|
|
|
|
|
|
|
# --- keyboard and mouse ----------------------------------------------------
|
|
|
|
|
# The touch UI is the whole story on a phone. On a machine with a keyboard -
|
|
|
|
|
# which in practice means the web build - WASD or the arrow keys steer, Q/E
|
|
|
|
|
# pivot the formation, and the wheel zooms. Clicking the icon buttons already
|
|
|
|
|
# works without any code here, because the project emulates touch from the
|
|
|
|
|
# mouse (see project.godot), which is the same path the buttons already read.
|
|
|
|
|
#
|
|
|
|
|
# Keys are tracked from events rather than polled off the Input singleton so
|
|
|
|
|
# the whole thing is drivable from a headless test.
|
|
|
|
|
const KEY_DIRECTIONS: Dictionary = {
|
|
|
|
|
KEY_W: Vector2.UP, KEY_UP: Vector2.UP,
|
|
|
|
|
KEY_S: Vector2.DOWN, KEY_DOWN: Vector2.DOWN,
|
|
|
|
|
KEY_A: Vector2.LEFT, KEY_LEFT: Vector2.LEFT,
|
|
|
|
|
KEY_D: Vector2.RIGHT, KEY_RIGHT: Vector2.RIGHT,
|
|
|
|
|
}
|
|
|
|
|
const KEY_PIVOT_CW: int = KEY_E
|
|
|
|
|
const KEY_PIVOT_CCW: int = KEY_Q
|
|
|
|
|
const WHEEL_ZOOM_STEP: float = 1.12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var keys_down: Dictionary = {}
|
|
|
|
|
var keyboard_seen: bool = false
|
|
|
|
|
var keyboard_steering: bool = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
#148: the main theme plays, and keeps playing
"All the time" is the whole of the requirement and it decides the shape.
The theme has to survive the main menu becoming the game, the game becoming
a planet surface, and the surface becoming the game again - so the player
cannot live in any of those scenes, because change_scene_to_file() frees
whichever one is current.
It is parented to the tree root instead, beside the GameState autoload,
which is the same trick for the same reason. Music.ensure() is idempotent by
node name, so each scene asks for the music on the way up and gets the one
already playing rather than a second copy over the top of it. Two call
sites cover everything: MainMenu, and GameController - which is the root
script of BOTH the space scene and the planet surface.
Done this way rather than as an autoload deliberately: project.godot carries
the developer's own uncommitted window settings, and adding music should not
drag those into a commit.
It is also PROCESS_MODE_ALWAYS. Since #128 a dialog stops the tree, and a
soundtrack that cuts out every time someone reads a line is worse than no
soundtrack.
The theme ships as MP3 rather than the 59MB WAV that was sitting in the
project root. Same 5m36s, 6.7MB, encoded with LAME at 160k - 89% smaller,
which is the difference between a repository that carries its music and one
that carries a WAV forever in its history. The original file is untouched
and still there, untracked, so nothing is lost.
The test checks CONTINUITY rather than "a sound started": the same player,
never a second one, through two scene changes and a dialog.
2026-08-29 13:05:21 +02:00
|
|
|
# #148: the main theme, which outlives this scene - see Music.ensure().
|
|
|
|
|
# This script is the root of BOTH the space scene and the planet
|
|
|
|
|
# surface, so one call here covers the whole game.
|
|
|
|
|
Music.ensure(get_tree())
|
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
|
|
|
if joystick != null:
|
|
|
|
|
joystick.direction_changed.connect(_on_joystick_direction)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _held_pivot_direction() -> float:
|
|
|
|
|
var dir: float = 0.0
|
|
|
|
|
if pivot_cw_button != null and pivot_cw_button.pressed_down:
|
|
|
|
|
dir += 1.0
|
|
|
|
|
if pivot_ccw_button != null and pivot_ccw_button.pressed_down:
|
|
|
|
|
dir -= 1.0
|
|
|
|
|
if keys_down.has(KEY_PIVOT_CW):
|
|
|
|
|
dir += 1.0
|
|
|
|
|
if keys_down.has(KEY_PIVOT_CCW):
|
|
|
|
|
dir -= 1.0
|
|
|
|
|
return clampf(dir, -1.0, 1.0)
|
|
|
|
|
|
|
|
|
|
|
#149, #153, #154: dialogs arrive, sit on top, and give the keyboard back
#149 - a line is typed out at about three and a half words a second, with a
scan sweep running down the panel and faint rules across it while it
decodes. The frame carries the charge while it works and settles when the
line lands; the caret is a solid write-head during the decode and only
starts blinking once it is waiting on the reader.
Two implementation notes worth keeping. The full text is set and then
REVEALED with visible_characters rather than the string being grown - a
growing string re-wraps on nearly every frame, so words jump between rows as
they are typed. And a press on a line still arriving FINISHES it instead of
dismissing it: without that, a player clicking at their usual pace skips the
second line having never seen a word of it, which is the exact thing #127
exists to prevent.
#153 - "after a dialog popup the input system is lost on keypress". Nothing
was lost; it was never delivered. A dialog stops the tree (#128), a paused
node receives no input, so a key RELEASED while the line was up never
reached the controller - keys_down kept it and the fleet flew on in that
direction once the game restarted.
NOTIFICATION_UNPAUSED arrives whatever a node's process mode is, which makes
it the one moment the question can be asked, and the physical keyboard is
the authority on what is actually held. That fixes anything that stops the
tree, not only dialogs, and the test checks it that way.
#154 - the banner was the ninth child of the UI layer and every panel came
after it, so all twelve of them drew over the top. It is the last child now,
in both scenes. Input order was never the problem and does not change: the
UI layer already sees unhandled input before the controllers do.
2026-08-29 14:39:07 +02:00
|
|
|
# #153: "after a dialog popup the godot input system is lost on keypress. If
|
|
|
|
|
# I had a key held it keeps moving in the key direction."
|
|
|
|
|
#
|
|
|
|
|
# It was not lost - it was never delivered. A dialog stops the tree (#128),
|
|
|
|
|
# a paused node receives no input, and this controller is paused along with
|
|
|
|
|
# everything else. So a key RELEASED while the line was up never reached
|
|
|
|
|
# _handle_key, keys_down kept it, and the fleet flew on in that direction
|
|
|
|
|
# once the game restarted.
|
|
|
|
|
#
|
|
|
|
|
# NOTIFICATION_UNPAUSED arrives whatever a node's process mode is, which
|
|
|
|
|
# makes it the one moment where the question can be asked. The physical
|
|
|
|
|
# keyboard is the authority on what is actually held down; keys_down is only
|
|
|
|
|
# a record of what we were told, and it is the record that went stale.
|
|
|
|
|
func _notification(what: int) -> void:
|
|
|
|
|
if what == NOTIFICATION_UNPAUSED:
|
|
|
|
|
_forget_released_keys()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _forget_released_keys() -> void:
|
|
|
|
|
for key in keys_down.keys():
|
|
|
|
|
if not Input.is_key_pressed(key):
|
|
|
|
|
keys_down.erase(key)
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
func keyboard_direction() -> Vector2:
|
|
|
|
|
var dir: Vector2 = Vector2.ZERO
|
|
|
|
|
for key in KEY_DIRECTIONS:
|
|
|
|
|
if keys_down.has(key):
|
|
|
|
|
dir += KEY_DIRECTIONS[key]
|
|
|
|
|
return Vector2.ZERO if dir == Vector2.ZERO else dir.normalized()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _handle_key(event: InputEventKey) -> void:
|
|
|
|
|
if event.echo:
|
|
|
|
|
return
|
|
|
|
|
if event.pressed:
|
|
|
|
|
keys_down[event.keycode] = true
|
|
|
|
|
_note_keyboard()
|
|
|
|
|
else:
|
|
|
|
|
keys_down.erase(event.keycode)
|
|
|
|
|
# A quick tap of a pivot key gets the same minimum step a tapped
|
|
|
|
|
# button does, rather than turning by a barely-visible sliver.
|
|
|
|
|
if event.keycode == KEY_PIVOT_CW:
|
|
|
|
|
_finish_pivot_tap(1.0)
|
|
|
|
|
elif event.keycode == KEY_PIVOT_CCW:
|
|
|
|
|
_finish_pivot_tap(-1.0)
|
|
|
|
|
|
|
|
|
|
|
Implement #85, #86, #87 and #88
#85 - on a computer the sticks and the pivot pair are dead weight: WASD
steers, the mouse aims, Q/E pivot. All four go together the first time a
keyboard or a mouse is used, and only where there is no touchscreen. A touch
device with a keyboard attached keeps everything, because a finger is still a
way to play it.
#86 - the default gun shoots faster (500 -> 850) and further (320 -> 620).
The interesting half is that a bolt now STOPS where the gun's range ends.
Projectile lifetime is derived from fire_range at launch rather than set
independently, so the distance a shot covers and the distance the aim guide
draws are one number. They were two, and they disagreed: the guide promised
320 while a bolt actually flew speed * lifetime = 750, so shots landed on
things well past the line saying where they would stop.
#87 - the ship that fires throws sparks, so a shot is visibly attached to the
vessel it came from; with a whole fleet shooting there was nothing tying the
two together, which started mattering once the player had to aim. Enemy deaths
gained a shockwave ring and roughly double the debris - a kill used to read as
a small red puff that was easy to miss in a fight.
#88 - everything moves 1.5x faster: fleet cruise, every vessel's max_speed and
acceleration, and every enemy's. Scaling the enemies too keeps it a change of
TEMPO rather than a difficulty cut, and scaling acceleration alongside speed
keeps the handling from #77's tuning intact.
Three tests carried assertions that were really about one balance pass:
test_enemy_mothership pinned a literal 35.0 and now compares against the
chaser, which is what "never reaches ramming speed" actually means;
test_planet_atmosphere_transition flew far enough at the new speed to reach
the landed garrison, and now clears it, since that test is about the handoff
and not about a fight it never meant to pick.
Also repaired real data loss, caught only because test_gas_burner failed:
Godot had silently re-serialised GasBurnerBuildingDefinition.tres and dropped
collected_outputs, capabilities, map_radius and map_phase, and had recreated a
stale copy of it at the pre-#84 root path. Without collected_outputs no
transporter ever collects a burner's energy. Both files restored from git.
It had also resurrected pre-#84 SNAPSHOTS of Main.tscn, Planet.tscn and
lunar_surface.gdshader at their old root paths - the root Main.tscn still
carried 27 references to the flat layout. They are dead weight the engine
never loads, since project.godot and every preload point into the folders, but
they are exactly the kind of thing that wastes an hour later when someone
edits the wrong copy. Deleted, and the root is now down to ui_theme.tres,
icon.svg, web_shell.html and the two config files.
Suite 119/119.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:19:14 +02:00
|
|
|
# #85: on a computer the on-screen sticks and the pivot pair are dead weight -
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
# WASD steers, the mouse aims, Q/E pivot.
|
Implement #85, #86, #87 and #88
#85 - on a computer the sticks and the pivot pair are dead weight: WASD
steers, the mouse aims, Q/E pivot. All four go together the first time a
keyboard or a mouse is used, and only where there is no touchscreen. A touch
device with a keyboard attached keeps everything, because a finger is still a
way to play it.
#86 - the default gun shoots faster (500 -> 850) and further (320 -> 620).
The interesting half is that a bolt now STOPS where the gun's range ends.
Projectile lifetime is derived from fire_range at launch rather than set
independently, so the distance a shot covers and the distance the aim guide
draws are one number. They were two, and they disagreed: the guide promised
320 while a bolt actually flew speed * lifetime = 750, so shots landed on
things well past the line saying where they would stop.
#87 - the ship that fires throws sparks, so a shot is visibly attached to the
vessel it came from; with a whole fleet shooting there was nothing tying the
two together, which started mattering once the player had to aim. Enemy deaths
gained a shockwave ring and roughly double the debris - a kill used to read as
a small red puff that was easy to miss in a fight.
#88 - everything moves 1.5x faster: fleet cruise, every vessel's max_speed and
acceleration, and every enemy's. Scaling the enemies too keeps it a change of
TEMPO rather than a difficulty cut, and scaling acceleration alongside speed
keeps the handling from #77's tuning intact.
Three tests carried assertions that were really about one balance pass:
test_enemy_mothership pinned a literal 35.0 and now compares against the
chaser, which is what "never reaches ramming speed" actually means;
test_planet_atmosphere_transition flew far enough at the new speed to reach
the landed garrison, and now clears it, since that test is about the handoff
and not about a fight it never meant to pick.
Also repaired real data loss, caught only because test_gas_burner failed:
Godot had silently re-serialised GasBurnerBuildingDefinition.tres and dropped
collected_outputs, capabilities, map_radius and map_phase, and had recreated a
stale copy of it at the pre-#84 root path. Without collected_outputs no
transporter ever collects a burner's energy. Both files restored from git.
It had also resurrected pre-#84 SNAPSHOTS of Main.tscn, Planet.tscn and
lunar_surface.gdshader at their old root paths - the root Main.tscn still
carried 27 references to the flat layout. They are dead weight the engine
never loads, since project.godot and every preload point into the folders, but
they are exactly the kind of thing that wastes an hour later when someone
edits the wrong copy. Deleted, and the root is now down to ui_theme.tres,
icon.svg, web_shell.html and the two config files.
Suite 119/119.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:19:14 +02:00
|
|
|
#
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
# #171: they now come and GO, rather than being taken away once and for all.
|
|
|
|
|
#
|
|
|
|
|
# The old rule asked DisplayServer.is_touchscreen_available() and, if it said
|
|
|
|
|
# yes, kept everything for ever - because the failure to avoid is a phone left
|
|
|
|
|
# with no way to steer. In a browser that query says yes on half the laptops
|
|
|
|
|
# in the world, so the web build showed thumb sticks to people holding a
|
|
|
|
|
# mouse, which is the ticket. It also could not be undone: a wrong answer the
|
|
|
|
|
# other way stranded a tablet with no controls until it was restarted.
|
|
|
|
|
#
|
|
|
|
|
# Following the LAST INPUT ACTUALLY USED fixes both ends. Touch the screen and
|
|
|
|
|
# the controls are there; use the mouse or the keyboard and they go. Nobody is
|
|
|
|
|
# ever stranded, because the gesture that needs them is the same gesture that
|
|
|
|
|
# brings them back, and it takes one tap. GameState owns the reading (see
|
|
|
|
|
# note_touch_input) so the mouse-aiming gate cannot disagree with this.
|
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
|
|
|
func _note_keyboard() -> void:
|
|
|
|
|
keyboard_seen = true
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
GameState.note_pointer_input()
|
|
|
|
|
_sync_touch_controls()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _note_touch() -> void:
|
|
|
|
|
GameState.note_touch_input()
|
|
|
|
|
_sync_touch_controls()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _sync_touch_controls() -> void:
|
|
|
|
|
var offered: bool = GameState.pointer_is_touch
|
Implement #85, #86, #87 and #88
#85 - on a computer the sticks and the pivot pair are dead weight: WASD
steers, the mouse aims, Q/E pivot. All four go together the first time a
keyboard or a mouse is used, and only where there is no touchscreen. A touch
device with a keyboard attached keeps everything, because a finger is still a
way to play it.
#86 - the default gun shoots faster (500 -> 850) and further (320 -> 620).
The interesting half is that a bolt now STOPS where the gun's range ends.
Projectile lifetime is derived from fire_range at launch rather than set
independently, so the distance a shot covers and the distance the aim guide
draws are one number. They were two, and they disagreed: the guide promised
320 while a bolt actually flew speed * lifetime = 750, so shots landed on
things well past the line saying where they would stop.
#87 - the ship that fires throws sparks, so a shot is visibly attached to the
vessel it came from; with a whole fleet shooting there was nothing tying the
two together, which started mattering once the player had to aim. Enemy deaths
gained a shockwave ring and roughly double the debris - a kill used to read as
a small red puff that was easy to miss in a fight.
#88 - everything moves 1.5x faster: fleet cruise, every vessel's max_speed and
acceleration, and every enemy's. Scaling the enemies too keeps it a change of
TEMPO rather than a difficulty cut, and scaling acceleration alongside speed
keeps the handling from #77's tuning intact.
Three tests carried assertions that were really about one balance pass:
test_enemy_mothership pinned a literal 35.0 and now compares against the
chaser, which is what "never reaches ramming speed" actually means;
test_planet_atmosphere_transition flew far enough at the new speed to reach
the landed garrison, and now clears it, since that test is about the handoff
and not about a fight it never meant to pick.
Also repaired real data loss, caught only because test_gas_burner failed:
Godot had silently re-serialised GasBurnerBuildingDefinition.tres and dropped
collected_outputs, capabilities, map_radius and map_phase, and had recreated a
stale copy of it at the pre-#84 root path. Without collected_outputs no
transporter ever collects a burner's energy. Both files restored from git.
It had also resurrected pre-#84 SNAPSHOTS of Main.tscn, Planet.tscn and
lunar_surface.gdshader at their old root paths - the root Main.tscn still
carried 27 references to the flat layout. They are dead weight the engine
never loads, since project.godot and every preload point into the folders, but
they are exactly the kind of thing that wastes an hour later when someone
edits the wrong copy. Deleted, and the root is now down to ui_theme.tres,
icon.svg, web_shell.html and the two config files.
Suite 119/119.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:19:14 +02:00
|
|
|
for control in [joystick, fire_stick, pivot_cw_button, pivot_ccw_button]:
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
if control != null and control.visible != offered:
|
|
|
|
|
control.visible = offered
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
func _button_holds(button: IconButton, at_position: Vector2) -> bool:
|
|
|
|
|
return button != null and Rect2(button.global_position, button.size).has_point(at_position)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _set_button_held(button: IconButton, held: bool) -> void:
|
|
|
|
|
if button == null or button.pressed_down == held:
|
|
|
|
|
return
|
|
|
|
|
button.pressed_down = held
|
|
|
|
|
button.queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Returns true when a pivot button consumed the touch.
|
|
|
|
|
func _handle_pivot_touch(event: InputEventScreenTouch) -> bool:
|
|
|
|
|
if event.pressed:
|
|
|
|
|
if _button_holds(pivot_cw_button, event.position) and pivot_cw_touch == -1:
|
|
|
|
|
pivot_cw_touch = event.index
|
|
|
|
|
_set_button_held(pivot_cw_button, true)
|
|
|
|
|
return true
|
|
|
|
|
if _button_holds(pivot_ccw_button, event.position) and pivot_ccw_touch == -1:
|
|
|
|
|
pivot_ccw_touch = event.index
|
|
|
|
|
_set_button_held(pivot_ccw_button, true)
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
if event.index == pivot_cw_touch:
|
|
|
|
|
pivot_cw_touch = -1
|
|
|
|
|
_set_button_held(pivot_cw_button, false)
|
|
|
|
|
_finish_pivot_tap(1.0)
|
|
|
|
|
return true
|
|
|
|
|
if event.index == pivot_ccw_touch:
|
|
|
|
|
pivot_ccw_touch = -1
|
|
|
|
|
_set_button_held(pivot_ccw_button, false)
|
|
|
|
|
_finish_pivot_tap(-1.0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _update_pivot(delta: float) -> void:
|
|
|
|
|
var dir: float = _held_pivot_direction()
|
|
|
|
|
if dir == 0.0:
|
|
|
|
|
return
|
|
|
|
|
var amount: float = PIVOT_SPEED * delta * dir
|
|
|
|
|
pivot_hold_amount += absf(amount)
|
|
|
|
|
_pivot(amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _finish_pivot_tap(direction: float) -> void:
|
|
|
|
|
# Fired on release. A press held long enough has already turned far
|
2026-08-17 19:47:50 +02:00
|
|
|
# enough on its own; a brief tap gets topped up to the minimum step -
|
|
|
|
|
# but only once THAT DIRECTION's own cooldown has cleared, so spamming
|
|
|
|
|
# one side can't grant rotation faster than holding the button would.
|
|
|
|
|
var cooldown_left: float = pivot_cw_tap_cooldown_left if direction > 0.0 else pivot_ccw_tap_cooldown_left
|
|
|
|
|
if pivot_hold_amount < PIVOT_MIN_STEP and cooldown_left <= 0.0:
|
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
|
|
|
_pivot((PIVOT_MIN_STEP - pivot_hold_amount) * direction)
|
2026-08-17 19:47:50 +02:00
|
|
|
if direction > 0.0:
|
|
|
|
|
pivot_cw_tap_cooldown_left = PIVOT_TAP_COOLDOWN
|
|
|
|
|
else:
|
|
|
|
|
pivot_ccw_tap_cooldown_left = PIVOT_TAP_COOLDOWN
|
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
|
|
|
pivot_hold_amount = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _pivot(radians: float) -> void:
|
|
|
|
|
# Turns the formation and re-seats the ships already flying it, so the
|
|
|
|
|
# change is visible immediately rather than only on the next rebuild.
|
|
|
|
|
# apply_formation() writes the new offsets into Fleet.formation_offsets,
|
|
|
|
|
# which move_to()/move_in_direction() also read - so a pivot applied
|
|
|
|
|
# while the joystick is steering survives the next steering update
|
|
|
|
|
# instead of being overwritten by the old layout.
|
|
|
|
|
GameState.rotate_formation(radians)
|
|
|
|
|
fleet.apply_formation()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_joystick_direction(direction: Vector2) -> void:
|
|
|
|
|
# Steering is re-issued every frame in _process while the stick is held,
|
|
|
|
|
# so this only needs to handle the release (stick back to centre).
|
|
|
|
|
if direction == Vector2.ZERO:
|
|
|
|
|
fleet.stop()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> void:
|
2026-08-17 19:47:50 +02:00
|
|
|
pivot_cw_tap_cooldown_left = maxf(pivot_cw_tap_cooldown_left - delta, 0.0)
|
|
|
|
|
pivot_ccw_tap_cooldown_left = maxf(pivot_ccw_tap_cooldown_left - delta, 0.0)
|
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
|
|
|
_update_pivot(delta)
|
|
|
|
|
|
|
|
|
|
# Keep the heading fresh while a control is held. Fleet advances its own
|
|
|
|
|
# anchor in _process, so this only needs to report the direction. The
|
|
|
|
|
# keyboard takes precedence, and releasing the keys has to stop the fleet
|
|
|
|
|
# itself - unlike the stick, there is no centred-again signal to hook.
|
#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
|
|
|
# #124: the breach drives. One check here covers the stick, the keyboard
|
|
|
|
|
# and the released-again path together - three separate gates would be
|
|
|
|
|
# three chances for one of them to keep steering.
|
2026-09-03 22:00:26 +02:00
|
|
|
if GameState.intro_locks_movement or GameState.encounter_locks_movement:
|
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
|
|
|
keyboard_steering = false
|
#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
|
|
|
else:
|
|
|
|
|
_apply_steering()
|
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
|
|
|
|
|
|
|
|
var target: Vector2 = fleet.get_centroid()
|
|
|
|
|
if camera.global_position.distance_to(target) <= camera_snap_distance:
|
|
|
|
|
# Snap and stop dead - see camera_snap_distance. Assigning the exact
|
|
|
|
|
# target means a settled fleet produces a bit-identical camera position
|
|
|
|
|
# frame after frame, so the background shaders stop re-rendering a
|
|
|
|
|
# shifted image and tearing has nothing to reveal.
|
|
|
|
|
camera.global_position = target
|
|
|
|
|
return
|
|
|
|
|
var t: float = clamp(camera_follow_speed * delta, 0.0, 1.0)
|
|
|
|
|
camera.global_position = camera.global_position.lerp(target, t)
|
|
|
|
|
|
|
|
|
|
|
#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
|
|
|
func _apply_steering() -> void:
|
|
|
|
|
var keyed: Vector2 = keyboard_direction()
|
|
|
|
|
if keyed != Vector2.ZERO:
|
|
|
|
|
keyboard_steering = true
|
|
|
|
|
fleet.move_in_direction(keyed)
|
|
|
|
|
elif keyboard_steering:
|
|
|
|
|
keyboard_steering = false
|
|
|
|
|
fleet.stop()
|
|
|
|
|
elif joystick != null and joystick.direction != Vector2.ZERO:
|
|
|
|
|
fleet.move_in_direction(joystick.direction)
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
|
|
|
if event is InputEventScreenTouch:
|
|
|
|
|
_handle_touch(event)
|
|
|
|
|
elif event is InputEventScreenDrag:
|
|
|
|
|
_handle_drag(event)
|
|
|
|
|
elif event is InputEventMagnifyGesture:
|
|
|
|
|
_handle_magnify(event)
|
|
|
|
|
elif event is InputEventKey:
|
|
|
|
|
_handle_key(event)
|
Implement #85, #86, #87 and #88
#85 - on a computer the sticks and the pivot pair are dead weight: WASD
steers, the mouse aims, Q/E pivot. All four go together the first time a
keyboard or a mouse is used, and only where there is no touchscreen. A touch
device with a keyboard attached keeps everything, because a finger is still a
way to play it.
#86 - the default gun shoots faster (500 -> 850) and further (320 -> 620).
The interesting half is that a bolt now STOPS where the gun's range ends.
Projectile lifetime is derived from fire_range at launch rather than set
independently, so the distance a shot covers and the distance the aim guide
draws are one number. They were two, and they disagreed: the guide promised
320 while a bolt actually flew speed * lifetime = 750, so shots landed on
things well past the line saying where they would stop.
#87 - the ship that fires throws sparks, so a shot is visibly attached to the
vessel it came from; with a whole fleet shooting there was nothing tying the
two together, which started mattering once the player had to aim. Enemy deaths
gained a shockwave ring and roughly double the debris - a kill used to read as
a small red puff that was easy to miss in a fight.
#88 - everything moves 1.5x faster: fleet cruise, every vessel's max_speed and
acceleration, and every enemy's. Scaling the enemies too keeps it a change of
TEMPO rather than a difficulty cut, and scaling acceleration alongside speed
keeps the handling from #77's tuning intact.
Three tests carried assertions that were really about one balance pass:
test_enemy_mothership pinned a literal 35.0 and now compares against the
chaser, which is what "never reaches ramming speed" actually means;
test_planet_atmosphere_transition flew far enough at the new speed to reach
the landed garrison, and now clears it, since that test is about the handoff
and not about a fight it never meant to pick.
Also repaired real data loss, caught only because test_gas_burner failed:
Godot had silently re-serialised GasBurnerBuildingDefinition.tres and dropped
collected_outputs, capabilities, map_radius and map_phase, and had recreated a
stale copy of it at the pre-#84 root path. Without collected_outputs no
transporter ever collects a burner's energy. Both files restored from git.
It had also resurrected pre-#84 SNAPSHOTS of Main.tscn, Planet.tscn and
lunar_surface.gdshader at their old root paths - the root Main.tscn still
carried 27 references to the flat layout. They are dead weight the engine
never loads, since project.godot and every preload point into the folders, but
they are exactly the kind of thing that wastes an hour later when someone
edits the wrong copy. Deleted, and the root is now down to ui_theme.tres,
icon.svg, web_shell.html and the two config files.
Suite 119/119.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:19:14 +02:00
|
|
|
elif event is InputEventMouseMotion:
|
|
|
|
|
_note_keyboard()
|
2026-08-23 21:59:24 +02:00
|
|
|
if speed_fader != null:
|
|
|
|
|
speed_fader.handle_mouse_motion(event)
|
|
|
|
|
elif event is InputEventMouseButton:
|
Implement #85, #86, #87 and #88
#85 - on a computer the sticks and the pivot pair are dead weight: WASD
steers, the mouse aims, Q/E pivot. All four go together the first time a
keyboard or a mouse is used, and only where there is no touchscreen. A touch
device with a keyboard attached keeps everything, because a finger is still a
way to play it.
#86 - the default gun shoots faster (500 -> 850) and further (320 -> 620).
The interesting half is that a bolt now STOPS where the gun's range ends.
Projectile lifetime is derived from fire_range at launch rather than set
independently, so the distance a shot covers and the distance the aim guide
draws are one number. They were two, and they disagreed: the guide promised
320 while a bolt actually flew speed * lifetime = 750, so shots landed on
things well past the line saying where they would stop.
#87 - the ship that fires throws sparks, so a shot is visibly attached to the
vessel it came from; with a whole fleet shooting there was nothing tying the
two together, which started mattering once the player had to aim. Enemy deaths
gained a shockwave ring and roughly double the debris - a kill used to read as
a small red puff that was easy to miss in a fight.
#88 - everything moves 1.5x faster: fleet cruise, every vessel's max_speed and
acceleration, and every enemy's. Scaling the enemies too keeps it a change of
TEMPO rather than a difficulty cut, and scaling acceleration alongside speed
keeps the handling from #77's tuning intact.
Three tests carried assertions that were really about one balance pass:
test_enemy_mothership pinned a literal 35.0 and now compares against the
chaser, which is what "never reaches ramming speed" actually means;
test_planet_atmosphere_transition flew far enough at the new speed to reach
the landed garrison, and now clears it, since that test is about the handoff
and not about a fight it never meant to pick.
Also repaired real data loss, caught only because test_gas_burner failed:
Godot had silently re-serialised GasBurnerBuildingDefinition.tres and dropped
collected_outputs, capabilities, map_radius and map_phase, and had recreated a
stale copy of it at the pre-#84 root path. Without collected_outputs no
transporter ever collects a burner's energy. Both files restored from git.
It had also resurrected pre-#84 SNAPSHOTS of Main.tscn, Planet.tscn and
lunar_surface.gdshader at their old root paths - the root Main.tscn still
carried 27 references to the flat layout. They are dead weight the engine
never loads, since project.godot and every preload point into the folders, but
they are exactly the kind of thing that wastes an hour later when someone
edits the wrong copy. Deleted, and the root is now down to ui_theme.tres,
icon.svg, web_shell.html and the two config files.
Suite 119/119.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:19:14 +02:00
|
|
|
_note_keyboard()
|
2026-08-23 21:59:24 +02:00
|
|
|
# #118: the fader gets first refusal, so dragging the boost does not
|
|
|
|
|
# also pan or zoom the world behind it.
|
|
|
|
|
if speed_fader != null and speed_fader.handle_mouse_button(event):
|
|
|
|
|
return
|
|
|
|
|
if not event.pressed:
|
|
|
|
|
return
|
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
|
|
|
# Wheel zoom, the desktop equivalent of the pinch.
|
|
|
|
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
|
|
|
|
_set_zoom(camera.zoom.x * WHEEL_ZOOM_STEP)
|
|
|
|
|
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
|
|
|
|
_set_zoom(camera.zoom.x / WHEEL_ZOOM_STEP)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _handle_touch(event: InputEventScreenTouch) -> void:
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
# #171: a finger on the glass is the one unambiguous piece of evidence
|
|
|
|
|
# that this is a touch device being played by touch, and it is recorded
|
|
|
|
|
# before anything gets a chance to claim the event - a touch swallowed by
|
|
|
|
|
# the joystick is still a touch.
|
|
|
|
|
_note_touch()
|
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
|
|
|
# The joystick gets first refusal on every touch, and only claims ones
|
|
|
|
|
# that begin inside its own rect - so pinch-to-zoom still works anywhere
|
|
|
|
|
# else on screen.
|
2026-08-15 04:45:15 +02:00
|
|
|
if gas_bomb_controller != null and gas_bomb_controller.handle_touch(event):
|
|
|
|
|
return
|
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
|
|
|
if laser_controller != null and laser_controller.handle_touch(event):
|
|
|
|
|
return
|
2026-09-02 13:43:56 +02:00
|
|
|
if dash_controller != null and dash_controller.handle_touch(event):
|
|
|
|
|
return
|
2026-08-16 17:59:04 +02:00
|
|
|
if speed_fader != null and speed_fader.handle_touch(event):
|
|
|
|
|
return
|
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
|
|
|
if _handle_pivot_touch(event):
|
|
|
|
|
return
|
|
|
|
|
if joystick != null and joystick.visible and joystick.handle_touch(event):
|
|
|
|
|
return
|
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
|
|
|
# Order between the two sticks does not matter: each only claims touches
|
|
|
|
|
# that begin inside its own rect, and their rects do not overlap.
|
|
|
|
|
if fire_stick != null and fire_stick.visible and fire_stick.handle_touch(event):
|
|
|
|
|
return
|
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
|
|
|
|
|
|
|
|
if event.pressed:
|
|
|
|
|
touches[event.index] = event.position
|
|
|
|
|
if touches.size() == 2:
|
|
|
|
|
_start_pinch()
|
|
|
|
|
if debug_input_logging:
|
|
|
|
|
print("DEBUG touch pressed idx=%d pos=%s active_touches=%d" % [event.index, event.position, touches.size()])
|
|
|
|
|
else:
|
|
|
|
|
touches.erase(event.index)
|
|
|
|
|
if touches.size() < 2:
|
|
|
|
|
pinch_active = false
|
|
|
|
|
if debug_input_logging:
|
|
|
|
|
print("DEBUG touch released idx=%d active_touches=%d" % [event.index, touches.size()])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _handle_drag(event: InputEventScreenDrag) -> void:
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
# A finger moving on the glass, same as a finger arriving on it (#171).
|
|
|
|
|
_note_touch()
|
2026-08-15 04:45:15 +02:00
|
|
|
if gas_bomb_controller != null and gas_bomb_controller.handle_drag(event):
|
|
|
|
|
return
|
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
|
|
|
if laser_controller != null and laser_controller.handle_drag(event):
|
|
|
|
|
return
|
2026-09-02 13:43:56 +02:00
|
|
|
if dash_controller != null and dash_controller.handle_drag(event):
|
|
|
|
|
return
|
2026-08-16 17:59:04 +02:00
|
|
|
if speed_fader != null and speed_fader.handle_drag(event):
|
|
|
|
|
return
|
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
|
|
|
if joystick != null and joystick.visible and joystick.handle_drag(event):
|
|
|
|
|
return
|
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
|
|
|
if fire_stick != null and fire_stick.visible and fire_stick.handle_drag(event):
|
|
|
|
|
return
|
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
|
|
|
if not touches.has(event.index):
|
|
|
|
|
return
|
|
|
|
|
touches[event.index] = event.position
|
|
|
|
|
|
|
|
|
|
if pinch_active and touches.size() == 2 and pinch_start_distance > 0.0:
|
|
|
|
|
var positions: Array = touches.values()
|
|
|
|
|
var current_distance: float = positions[0].distance_to(positions[1])
|
|
|
|
|
# Spreading the fingers apart zooms IN. Camera2D.zoom is a
|
|
|
|
|
# magnification (larger = closer), so the ratio goes
|
|
|
|
|
# current/start, not start/current.
|
|
|
|
|
var new_zoom: float = pinch_start_zoom * (current_distance / max(pinch_start_distance, 1.0))
|
|
|
|
|
_set_zoom(new_zoom)
|
|
|
|
|
|
|
|
|
|
if debug_input_logging:
|
|
|
|
|
print("DEBUG pinch drag distance=%.1f zoom=%.3f" % [current_distance, camera.zoom.x])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _handle_magnify(event: InputEventMagnifyGesture) -> void:
|
|
|
|
|
# Fallback path: some platforms (and desktop trackpads) report pinch as a
|
|
|
|
|
# single semantic gesture instead of two raw touch streams. factor > 1
|
|
|
|
|
# means the gesture is spreading outward, which zooms in - and since
|
|
|
|
|
# Camera2D.zoom is a magnification, that multiplies.
|
|
|
|
|
_set_zoom(camera.zoom.x * event.factor)
|
|
|
|
|
if debug_input_logging:
|
|
|
|
|
print("DEBUG magnify gesture factor=%.3f zoom=%.3f" % [event.factor, camera.zoom.x])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _start_pinch() -> void:
|
|
|
|
|
pinch_active = true
|
|
|
|
|
var positions: Array = touches.values()
|
|
|
|
|
pinch_start_distance = positions[0].distance_to(positions[1])
|
|
|
|
|
pinch_start_zoom = camera.zoom.x
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _set_zoom(raw_zoom: float) -> void:
|
|
|
|
|
var z: float = clamp(raw_zoom, min_zoom, max_zoom)
|
|
|
|
|
camera.zoom = Vector2(z, z)
|