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 Area2D
|
|
|
|
|
|
|
|
|
|
signal destroyed
|
|
|
|
|
|
#84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it
touched and finding the file you wanted meant knowing its exact name. Twelve
folders now, grouped by what a thing IS rather than what type it is - a
vessel's definition, its component scenes and its upgrade resources sit
together, because that is the set you read when you change one of them.
buildings core controllers enemies logistics planets
resources shaders surface ui vessels vfx world
332 files moved and every res:// reference rewritten - 306 files touched,
including the autoload and main-scene paths in project.godot. Nothing renamed:
this is purely where files sit, so the diff is a pile of pure moves and no
behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at
the root, where Godot and the export preset expect them.
Verified three ways, because a suite alone would not have caught all of it:
the full 117 pass, the web export builds clean, and the game renders.
The export is why that matters. The FIRST export after the move failed with
"Attempt to open script 'res://GameState.gd'" - the old path - while
project.godot on disk plainly said res://core/GameState.gd. That is this
project's documented Android FUSE mtime trap: Godot served a cached copy of a
file whose content had changed. A `touch` fixed it. Nothing in the test suite
would have found that, because the suite runs from a copy on a native
filesystem - the one place the bug cannot happen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
|
|
|
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
|
2026-08-14 22:12:57 +02:00
|
|
|
|
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
|
|
|
@export var max_speed: float = 460.0
|
|
|
|
|
@export var acceleration: float = 450.0
|
|
|
|
|
@export var rotation_speed: float = 8.0
|
|
|
|
|
@export var arrive_radius: float = 6.0
|
2026-08-11 22:50:09 +02:00
|
|
|
@export var vessel_type: String = "interceptor"
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource
|
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 glow_color: Color = Color(0.25, 0.85, 1.0)
|
2026-08-14 22:12:57 +02:00
|
|
|
var core_color: Color = Color.WHITE
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
# The ship's TRUE world velocity - what `position` is actually integrated
|
|
|
|
|
# from, and the single source of truth for how fast this vessel is moving.
|
|
|
|
|
# Nothing else may contribute motion; see the note in _physics_process.
|
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 velocity: Vector2 = Vector2.ZERO
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
# The velocity of this ship's formation SLOT (the fleet anchor's own travel).
|
|
|
|
|
# Fed forward into the steering below rather than added on top of it: matching
|
|
|
|
|
# the slot's speed is most of the job of holding formation, so supplying it
|
|
|
|
|
# directly leaves `velocity` free to be a small correction and lets the ship
|
|
|
|
|
# sit in its slot with no steady-state lag and no teleporting.
|
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 fleet_velocity: Vector2 = Vector2.ZERO
|
|
|
|
|
var move_target: Vector2
|
2026-08-14 22:12:57 +02:00
|
|
|
|
|
|
|
|
# Compatibility accessors keep old saves/tests readable while shield state
|
|
|
|
|
# itself belongs exclusively to the optional ShieldComponent.
|
|
|
|
|
var shield_charges: int:
|
|
|
|
|
get:
|
|
|
|
|
var component: Node = get_component("shield")
|
|
|
|
|
return component.charges if component != null else 0
|
|
|
|
|
set(value):
|
|
|
|
|
var component: Node = get_component("shield")
|
|
|
|
|
if component != null:
|
|
|
|
|
component.set_charges(value)
|
|
|
|
|
|
|
|
|
|
var shield_recharge_left: float:
|
|
|
|
|
get:
|
|
|
|
|
var component: Node = get_component("shield")
|
|
|
|
|
return component.recharge_left if component != null else 0.0
|
|
|
|
|
set(value):
|
|
|
|
|
var component: Node = get_component("shield")
|
|
|
|
|
if component != null:
|
|
|
|
|
component.recharge_left = value
|
|
|
|
|
|
|
|
|
|
var projectile_damage: int:
|
|
|
|
|
get:
|
|
|
|
|
var component: Node = get_component("weapon")
|
|
|
|
|
return component.projectile_damage if component != null else 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
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
move_target = position
|
|
|
|
|
add_to_group("player_ships")
|
|
|
|
|
area_entered.connect(_on_area_entered)
|
2026-08-14 22:12:57 +02:00
|
|
|
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
|
|
|
|
if definition == null:
|
|
|
|
|
return
|
|
|
|
|
glow_color = definition.glow_color
|
|
|
|
|
for component_scene in definition.component_scenes:
|
|
|
|
|
var component: Node = component_scene.instantiate()
|
|
|
|
|
add_child(component)
|
|
|
|
|
if component.has_method("setup"):
|
|
|
|
|
component.setup()
|
2026-08-11 22:50:09 +02:00
|
|
|
apply_vessel_stats()
|
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
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func get_component(capability: String) -> Node:
|
|
|
|
|
for child in get_children():
|
|
|
|
|
if child.get("capability_id") == capability:
|
|
|
|
|
return child
|
|
|
|
|
return null
|
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
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func has_capability(capability: String) -> bool:
|
|
|
|
|
return get_component(capability) != null
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func set_move_target(world_pos: Vector2) -> void:
|
|
|
|
|
move_target = world_pos
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func apply_vessel_stats() -> void:
|
|
|
|
|
if definition == null:
|
|
|
|
|
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
|
|
|
|
if definition == null:
|
2026-08-11 22:50:09 +02:00
|
|
|
return
|
2026-08-14 22:12:57 +02:00
|
|
|
max_speed = GameState.vessel_stat(vessel_type, "max_speed")
|
|
|
|
|
acceleration = GameState.vessel_stat(vessel_type, "acceleration")
|
|
|
|
|
for child in get_children():
|
|
|
|
|
if child.has_method("apply_stats"):
|
|
|
|
|
child.apply_stats()
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func has_shield() -> bool:
|
|
|
|
|
var component: Node = get_component("shield")
|
|
|
|
|
return component != null and component.is_active()
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
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 _physics_process(delta: float) -> void:
|
2026-08-14 22:12:57 +02:00
|
|
|
for child in get_children():
|
|
|
|
|
if child.has_method("tick"):
|
|
|
|
|
child.tick(delta)
|
|
|
|
|
|
2026-08-16 17:59:04 +02:00
|
|
|
# #59: multiplied here, at the point of use, rather than baked into the
|
|
|
|
|
# cached max_speed/acceleration fields apply_vessel_stats() sets - those
|
|
|
|
|
# stay the true base values, so a stat upgrade researched mid-boost can't
|
|
|
|
|
# corrupt them, and the fader takes effect the instant it moves with no
|
|
|
|
|
# re-apply needed. arrive_radius is scaled too: at N physics ticks/sec a
|
|
|
|
|
# ship's per-tick step is `speed * delta`, and once that step outgrows
|
|
|
|
|
# the (otherwise fixed) arrive_radius it can overshoot the snap-to-target
|
|
|
|
|
# zone every tick while chasing a target that is ALSO now receding N
|
|
|
|
|
# times faster - a small, boost-proportional radius keeps the same
|
|
|
|
|
# "snaps onto its slot" formation tightness at any boost level that
|
|
|
|
|
# 1.0x already has.
|
|
|
|
|
var boost: float = GameState.fleet_speed_multiplier
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
var effective_acceleration: float = acceleration * boost
|
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 to_target: Vector2 = move_target - position
|
|
|
|
|
var distance: float = to_target.length()
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
|
|
|
|
|
# Snap only when the slot is STANDING STILL. A stationary target is the
|
|
|
|
|
# one case where converging asymptotically would leave a permanent
|
|
|
|
|
# sub-pixel residue, and settling exactly is what "the fleet has arrived"
|
|
|
|
|
# means everywhere else in the game. While the slot is moving the ship
|
|
|
|
|
# steers continuously instead - a snap there would be a teleport, which
|
|
|
|
|
# is what used to make `velocity` read zero for a ship that was in fact
|
|
|
|
|
# travelling at cruise speed, and that lie is what the whole
|
|
|
|
|
# stop()/fleet_velocity bookkeeping tangle existed to paper over.
|
|
|
|
|
if fleet_velocity == Vector2.ZERO and distance <= arrive_radius * boost:
|
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
|
|
|
position = move_target
|
|
|
|
|
velocity = Vector2.ZERO
|
|
|
|
|
else:
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
# Feed-forward plus correction. The ship aims to travel at its slot's
|
|
|
|
|
# velocity, plus whatever extra closes the gap to the slot - so
|
|
|
|
|
# holding formation costs no error, and `velocity` stays the ship's
|
|
|
|
|
# real motion rather than a residual that has to be added to
|
|
|
|
|
# something else to mean anything.
|
|
|
|
|
var correction: Vector2 = Vector2.ZERO
|
|
|
|
|
if distance > 0.001:
|
|
|
|
|
var braking_speed: float = sqrt(2.0 * effective_acceleration * distance)
|
|
|
|
|
correction = to_target.normalized() * min(max_speed * boost, braking_speed)
|
|
|
|
|
var desired_velocity: Vector2 = fleet_velocity + correction
|
|
|
|
|
# HARD BOUND on how fast any single vessel may travel, applied to the
|
|
|
|
|
# total and re-applied every tick. Feed-forward plus correction can
|
|
|
|
|
# ask for more than a vessel can actually fly, and without this the
|
|
|
|
|
# excess was free speed: a ship that had been handed a large velocity
|
|
|
|
|
# kept it, decaying only at `acceleration`. Clamping the REQUEST is
|
|
|
|
|
# not enough - the clamp has to sit on the integrated result, so no
|
|
|
|
|
# sequence of commands can leave a vessel travelling faster than its
|
|
|
|
|
# own top speed.
|
|
|
|
|
desired_velocity = desired_velocity.limit_length(max_speed * boost)
|
2026-08-16 17:59:04 +02:00
|
|
|
velocity = velocity.move_toward(desired_velocity, effective_acceleration * delta)
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
velocity = velocity.limit_length(max_speed * boost)
|
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
|
|
|
position += velocity * delta
|
|
|
|
|
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
if velocity.length() > 8.0:
|
|
|
|
|
var target_angle: float = velocity.angle() + PI / 2.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
|
|
|
rotation = lerp_angle(rotation, target_angle, rotation_speed * delta)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 14:43:29 +02:00
|
|
|
func take_damage(_amount: int) -> bool:
|
|
|
|
|
# Ships have no hit points - any unabsorbed hit is lethal. A shield
|
|
|
|
|
# charge absorbs a hit entirely regardless of its amount, matching
|
|
|
|
|
# VesselShieldComponent's existing all-or-nothing semantics; `_amount`
|
|
|
|
|
# is accepted (not `int`-clamped away) so callers like EnemyProjectile
|
|
|
|
|
# have a normal damage parameter to pass even though it isn't graded here.
|
2026-08-14 22:12:57 +02:00
|
|
|
for child in get_children():
|
|
|
|
|
if child.has_method("absorb_hit") and child.absorb_hit():
|
2026-08-15 14:43:29 +02:00
|
|
|
return true
|
#130, #135: a vessel's death takes a moment, and leaves an interceptor
#130 asked for the hull to break away from the fleet, drift a few metres,
and go up further off with something more explosive than a light animation.
The wreck is a separate node rather than the Ship staying alive to animate,
and that is the load-bearing part. A dying Ship still in the tree is still
an Area2D in the player_ships group with live weapon components - it would
keep firing, keep being fired at, keep counting as a target, and keep
colliding on the way out. So the ship's own lifecycle is untouched: it
leaves the fleet, emits destroyed, and is freed on exactly the frame it
always was. What is left behind is scenery, and the test pins that.
The explosion at the end was also the quieter of the two the game has.
Killing an enemy was made deliberately loud by #87 while losing one of your
own kept the base class's twelve-particle default - the reward louder than
the loss. It is now the bigger of the two.
#135 is the same scene from the other end: the prologue's convoy flies its
escorts on the corners of the box, which is the exposed part of it, and the
debris belt can take all four. With no interceptor left, the only hulls the
vortex could spare were holds, and the player flew out of the prologue in a
carrier. Two guards now: one interceptor is put back into the convoy at the
top of the vortex, where a vessel appearing among ten being thrown around a
black hole is not something anyone will catch; and the handover checks the
TYPES it hands over rather than only the count - one surviving carrier is
also a fleet of one, and it is not the fleet a new game starts with.
2026-08-28 15:04:26 +02:00
|
|
|
# #130: the hull comes off the formation and explodes further out, rather
|
|
|
|
|
# than going up on the spot. The wreck is its own node - this one is
|
|
|
|
|
# still freed on this frame, so nothing about who is in the fleet, who
|
|
|
|
|
# can be shot, or who is a target changes.
|
|
|
|
|
ShipWreckVFX.spawn(get_tree().root, self, _breakaway_direction())
|
2026-08-11 22:50:09 +02:00
|
|
|
destroyed.emit()
|
|
|
|
|
queue_free()
|
2026-08-15 14:43:29 +02:00
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
#130, #135: a vessel's death takes a moment, and leaves an interceptor
#130 asked for the hull to break away from the fleet, drift a few metres,
and go up further off with something more explosive than a light animation.
The wreck is a separate node rather than the Ship staying alive to animate,
and that is the load-bearing part. A dying Ship still in the tree is still
an Area2D in the player_ships group with live weapon components - it would
keep firing, keep being fired at, keep counting as a target, and keep
colliding on the way out. So the ship's own lifecycle is untouched: it
leaves the fleet, emits destroyed, and is freed on exactly the frame it
always was. What is left behind is scenery, and the test pins that.
The explosion at the end was also the quieter of the two the game has.
Killing an enemy was made deliberately loud by #87 while losing one of your
own kept the base class's twelve-particle default - the reward louder than
the loss. It is now the bigger of the two.
#135 is the same scene from the other end: the prologue's convoy flies its
escorts on the corners of the box, which is the exposed part of it, and the
debris belt can take all four. With no interceptor left, the only hulls the
vortex could spare were holds, and the player flew out of the prologue in a
carrier. Two guards now: one interceptor is put back into the convoy at the
top of the vortex, where a vessel appearing among ten being thrown around a
black hole is not something anyone will catch; and the handover checks the
TYPES it hands over rather than only the count - one surviving carrier is
also a fleet of one, and it is not the fleet a new game starts with.
2026-08-28 15:04:26 +02:00
|
|
|
# Which way is "out of the fleet". Away from the formation's own centre, so a
|
|
|
|
|
# wreck leaves the wedge instead of tumbling back through it; a ship flying
|
|
|
|
|
# alone - or one not under a Fleet at all - simply carries on the way it was
|
|
|
|
|
# already pointed.
|
|
|
|
|
func _breakaway_direction() -> Vector2:
|
|
|
|
|
var parent: Node = get_parent()
|
|
|
|
|
if parent != null and parent.has_method("get_centroid"):
|
|
|
|
|
var out: Vector2 = position - (parent.get_centroid() as Vector2)
|
|
|
|
|
if out.length() > 1.0:
|
|
|
|
|
return out.normalized()
|
|
|
|
|
if velocity.length() > 1.0:
|
|
|
|
|
return velocity.normalized()
|
|
|
|
|
return Vector2.UP.rotated(rotation)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 14:43:29 +02:00
|
|
|
func _on_area_entered(area: Area2D) -> void:
|
|
|
|
|
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
|
|
|
|
|
return
|
Implement #80, #81, #82 and #83
#80 - desktop combat. The mouse aims, the left button fires, and the number
keys pick the weapon: 1 the fleet's guns, 2 and up the abilities. The twin
sticks are built for thumbs; on a desktop they mean dragging a virtual stick
with a pointer that could simply BE the crosshair.
The pointer path hands its heading to FireStickController rather than driving
weapons itself, so exactly one thing in the scene aims a gun, and a held stick
wins - a finger on it is a deliberate act, a mouse resting somewhere is not.
Abilities gained fire_at(), which aims and fires in one call instead of
pretending to be a press-drag-release touch.
It is gated on the absence of a touchscreen, because Godot emulates a mouse
button from every touch and without that a tap on a phone would fire the guns.
The gate has an explicit override, and not for tidiness: headless reports a
touchscreen, so with the automatic answer alone the aiming could never be
exercised by a test and the only covered behaviour would be the guard.
#81 - the bottom-left control hint is gone, from both scenes and from
GameController.
#82 - a chaser is destroyed by the impact when it reaches a vessel. It used to
pass through the wreck untouched and line up another pass, which made a
ramming enemy strictly better than any other. Data-driven per enemy
(dies_on_contact), because a sniper that happens to drift into a ship has not
spent itself doing it - and routed through take_damage, so it dies exactly as
it dies to gunfire. The enemy is told about the impact BEFORE the ship
resolves its own damage, since resolving it can free the ship.
#83 - the research tree's buy button is gone; tapping a node twice buys it.
Deliberately "the same node twice" rather than a timed double-tap: no window
to miss on a slow phone, no accidental purchase while browsing, and the first
tap always does something visible, so the second is never a blind commitment.
The status line now says "Tap again to research" - with the button gone it is
the only thing distinguishing an affordable node from an unaffordable one.
test_enemy_collision asserted the exact behaviour #82 reversed, and now checks
the ram costs both. Suite 117/117.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:49:47 +02:00
|
|
|
# The enemy is told about the impact BEFORE this ship resolves its own
|
|
|
|
|
# damage, because resolving it can free this node - and a ship that dies
|
|
|
|
|
# to a ramming chaser must still take the chaser with it. Whether the
|
|
|
|
|
# impact actually destroys the enemy is the enemy's decision (#82).
|
|
|
|
|
if area.is_in_group("enemies") and area.has_method("collide_with_player_vessel"):
|
|
|
|
|
area.collide_with_player_vessel()
|
2026-08-15 14:43:29 +02:00
|
|
|
var absorbed: bool = take_damage(1)
|
|
|
|
|
if absorbed and area.is_in_group("asteroids") and is_instance_valid(area):
|
|
|
|
|
area.queue_free()
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
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 _draw() -> void:
|
2026-08-14 22:12:57 +02:00
|
|
|
var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([
|
|
|
|
|
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
|
|
|
|
])
|
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
|
|
|
NeonDraw.draw_glowing_polygon(self, points, glow_color, 1.0, core_color)
|