#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>
This commit is contained in:
Loïc Wisniewski 2026-08-21 19:44:14 +02:00
commit 2f161621c3
495 changed files with 571 additions and 567 deletions

39
logistics/Hauler.gd Normal file
View file

@ -0,0 +1,39 @@
extends Node2D
# Purely visual: an automated ore hauler running an extractor's route. It
# carries no state of its own - GameState owns the simulation and this node
# is created, positioned and freed to match it. See HaulerField.
const HULL_COLOR: Color = Color(0.55, 0.85, 0.4)
var laden: bool = false
func _ready() -> void:
add_to_group("haulers")
z_index = -1
queue_redraw()
func set_laden(value: bool) -> void:
if laden == value:
return
laden = value
queue_redraw()
func _draw() -> void:
# Blunt freighter silhouette - deliberately unlike the player's sharp
# triangles, so it is obvious at a glance this is not a fleet vessel.
var hull: PackedVector2Array = PackedVector2Array([
Vector2(0, -20), Vector2(11, -6), Vector2(11, 16), Vector2(-11, 16), Vector2(-11, -6),
])
NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 1.0)
if laden:
# Ore in the hold on the outbound leg, so a glance tells you which
# way it is going.
var cargo: PackedVector2Array = PackedVector2Array()
for i in range(6):
var a: float = (float(i) / 6.0) * TAU - PI / 2.0
cargo.append(Vector2(cos(a), sin(a)) * 6.0 + Vector2(0, 4))
NeonDraw.draw_glowing_polygon(self, cargo, Color(0.0, 0.62, 0.7), 1.0)

1
logistics/Hauler.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://dpmpi0bq7ncjb

6
logistics/Hauler.tscn Normal file
View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://logistics/Hauler.gd" id="1"]
[node name="Hauler" type="Node2D"]
script = ExtResource("1")

57
logistics/HaulerField.gd Normal file
View file

@ -0,0 +1,57 @@
extends Node2D
# Draws each extractor's hauler ONLY while it is somewhere the player could
# see it, and rebuilds it from GameState every frame.
#
# The point is that the visuals are optional. GameState simulates the round
# trip and pays out on arrival regardless of what exists in the scene, so a
# route far from the player costs nothing but still earns - while flying
# between a planet and the mothership shows the hauler making the run,
# positioned from the very same `progress` value that triggers the payout.
@export var visible_radius: float = 2600.0
@onready var fleet: Node2D = get_node("../Fleet")
var hauler_scene: PackedScene = preload("res://logistics/Hauler.tscn")
# extractor index -> live Hauler node
var active: Dictionary = {}
func _process(_delta: float) -> void:
var centre: Vector2 = fleet.get_centroid()
for i in range(GameState.extractors.size()):
var extractor: Dictionary = GameState.extractors[i]
var pos: Variant = GameState.get_hauler_position(extractor)
# No mothership means no route, so nothing to show.
if pos == null or pos.distance_to(centre) > visible_radius:
_despawn(i)
continue
var hauler: Node2D = active.get(i)
if hauler == null or not is_instance_valid(hauler):
hauler = hauler_scene.instantiate()
get_parent().add_child(hauler)
active[i] = hauler
var previous: Vector2 = hauler.position
hauler.position = pos
# Laden on the outbound leg, empty on the way back.
hauler.set_laden(extractor.progress <= GameState.HAULER_ARRIVAL)
var travel: Vector2 = pos - previous
if travel.length() > 0.5:
hauler.rotation = travel.angle() + PI / 2.0
# Drop visuals for extractors that no longer exist.
for key in active.keys():
if key >= GameState.extractors.size():
_despawn(key)
func _despawn(index: int) -> void:
var hauler: Node2D = active.get(index)
if hauler != null and is_instance_valid(hauler):
hauler.queue_free()
active.erase(index)

View file

@ -0,0 +1 @@
uid://cpjtke7w4pv0a

38
logistics/Transporter.gd Normal file
View file

@ -0,0 +1,38 @@
extends Node2D
# Purely visual: a transport vessel running a hub -> mothership -> building
# supply route. Carries no state of its own; GameState owns the simulation
# and TransporterField creates, positions and frees this to match.
const HULL_COLOR: Color = Color(0.95, 0.55, 0.85)
var laden: bool = false
func _ready() -> void:
add_to_group("transporters")
z_index = -1
queue_redraw()
func set_laden(value: bool) -> void:
if laden == value:
return
laden = value
queue_redraw()
func _draw() -> void:
# Squat and boxy, in the transport hub's own magenta, so it reads as
# "belongs to the hub" and not as an extractor hauler (green) or a fleet
# vessel (sharp cyan triangle).
var hull: PackedVector2Array = PackedVector2Array([
Vector2(0, -16), Vector2(10, -4), Vector2(10, 14), Vector2(-10, 14), Vector2(-10, -4),
])
NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 1.0)
if laden:
# A visible payload, so a loaded run is distinguishable from a
# deadhead back to the hub at a glance.
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
Vector2(-5, 0), Vector2(5, 0), Vector2(5, 9), Vector2(-5, 9),
]), HULL_COLOR, 0.85)

View file

@ -0,0 +1 @@
uid://u8bqm1f03l2y

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://logistics/Transporter.gd" id="1"]
[node name="Transporter" type="Node2D"]
script = ExtResource("1")

View file

@ -0,0 +1,51 @@
extends Node2D
# Draws the transport vessels GameState is simulating, and only while they
# are somewhere the player could see them. Same arrangement as HaulerField,
# and for the same reason: the simulation is authoritative and the visuals
# are optional, so a run happening far away costs nothing but still moves
# ore. Position comes from the very same `progress` that decides when the
# ore actually changes hands.
@export var visible_radius: float = 2600.0
@onready var fleet: Node2D = get_node("../Fleet")
var transporter_scene: PackedScene = preload("res://logistics/Transporter.tscn")
# index into GameState.transporters -> live node
var active: Dictionary = {}
func _process(_delta: float) -> void:
var centre: Vector2 = fleet.get_centroid()
for i in range(GameState.transporters.size()):
var t: Dictionary = GameState.transporters[i]
var pos: Vector2 = GameState.transporter_position(t)
if pos.distance_to(centre) > visible_radius:
_despawn(i)
continue
var node: Node2D = active.get(i)
if node == null or not is_instance_valid(node):
node = transporter_scene.instantiate()
get_parent().add_child(node)
active[i] = node
var previous: Vector2 = node.position
node.position = pos
node.set_laden(int(t.carrying) > 0)
var travel: Vector2 = pos - previous
if travel.length() > 0.5:
node.rotation = travel.angle() + PI / 2.0
for key in active.keys():
if key >= GameState.transporters.size():
_despawn(key)
func _despawn(index: int) -> void:
var node: Node2D = active.get(index)
if node != null and is_instance_valid(node):
node.queue_free()
active.erase(index)

View file

@ -0,0 +1 @@
uid://mrx6p58dog66

View file

@ -0,0 +1,18 @@
extends Node2D
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
# Visual-only craft-to-mothership flight. GameState owns position/progress;
# this node just renders a finished hull headed toward the hangar.
var vessel_type: String = "interceptor"
func _ready() -> void:
z_index = -1
queue_redraw()
func _draw() -> void:
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type)
if definition != null:
NeonDraw.draw_glowing_polygon(self, definition.hull_points, definition.glow_color, 1.0)

View file

@ -0,0 +1 @@
uid://ft0puvels7lg

View file

@ -0,0 +1,37 @@
extends Node2D
@export var visible_radius: float = 2600.0
@onready var fleet: Node2D = get_node("../Fleet")
var delivery_script: Script = preload("res://logistics/VesselDelivery.gd")
var active: Dictionary = {}
func _process(_delta: float) -> void:
var centre: Vector2 = fleet.get_centroid()
for i in range(GameState.vessel_deliveries.size()):
var delivery: Dictionary = GameState.vessel_deliveries[i]
var pos := GameState.vessel_delivery_position(delivery)
if pos.distance_to(centre) > visible_radius:
_despawn(i)
continue
var node: Node2D = active.get(i)
if node == null or not is_instance_valid(node):
node = delivery_script.new()
node.vessel_type = String(delivery.type)
get_parent().add_child(node)
active[i] = node
node.position = pos
var direction: Vector2 = (delivery.to as Vector2) - (delivery.from as Vector2)
if direction.length() > 0.1:
node.rotation = direction.angle() + PI / 2.0
node.queue_redraw()
for key in active.keys():
if key >= GameState.vessel_deliveries.size():
_despawn(key)
func _despawn(index: int) -> void:
var node: Node2D = active.get(index)
if node != null and is_instance_valid(node):
node.queue_free()
active.erase(index)

View file

@ -0,0 +1 @@
uid://clgmyishafevr