#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:
parent
baac11ae8b
commit
2f161621c3
495 changed files with 571 additions and 567 deletions
37
vfx/AsteroidExplosionVFX.gd
Normal file
37
vfx/AsteroidExplosionVFX.gd
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
class_name AsteroidExplosionVFX
|
||||
extends ExplosionVFXBase
|
||||
|
||||
# Tumbling rock-chunk debris, distinct from the energy-ship light-burst
|
||||
# style - asteroids are matter, not light.
|
||||
|
||||
const COLOR: Color = Color(0.65, 0.5, 0.35)
|
||||
|
||||
|
||||
static func spawn(parent: Node, world_position: Vector2) -> void:
|
||||
var vfx: AsteroidExplosionVFX = AsteroidExplosionVFX.new()
|
||||
vfx.position = world_position
|
||||
parent.add_child(vfx)
|
||||
|
||||
|
||||
func _random_particle_size() -> float:
|
||||
return randf_range(3.0, 7.0)
|
||||
|
||||
|
||||
# Debris tumbles independently of its flight path, unlike the light-burst
|
||||
# streaks - each chunk needs its own evolving rotation, so the base class
|
||||
# must keep writing visual.rotation every frame instead of leaving it fixed.
|
||||
func _particle_spins() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func _configure_particle(visual: ExplosionParticle, p: Dictionary) -> void:
|
||||
var s: float = p["size"]
|
||||
# Unrotated local points - the child node's own `rotation` (driven by
|
||||
# the base class from p["rot"]) supplies the tumble that used to be
|
||||
# baked into a fresh pt.rotated(p["rot"]) call every frame.
|
||||
var local_pts: PackedVector2Array = PackedVector2Array([
|
||||
Vector2(-s, -s * 0.6), Vector2(s * 0.7, -s * 0.5),
|
||||
Vector2(s, s * 0.5), Vector2(-s * 0.6, s),
|
||||
])
|
||||
visual.draw_callable = func(item: CanvasItem) -> void:
|
||||
item.draw_colored_polygon(local_pts, COLOR)
|
||||
1
vfx/AsteroidExplosionVFX.gd.uid
Normal file
1
vfx/AsteroidExplosionVFX.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dfy47yln6oyhx
|
||||
12
vfx/EnemyExplosionVFX.gd
Normal file
12
vfx/EnemyExplosionVFX.gd
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class_name EnemyExplosionVFX
|
||||
extends LightBurstVFX
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
color = Color(1.0, 0.15, 0.2)
|
||||
|
||||
|
||||
static func spawn(parent: Node, world_position: Vector2) -> void:
|
||||
var vfx: EnemyExplosionVFX = EnemyExplosionVFX.new()
|
||||
vfx.position = world_position
|
||||
parent.add_child(vfx)
|
||||
1
vfx/EnemyExplosionVFX.gd.uid
Normal file
1
vfx/EnemyExplosionVFX.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cblt8bqc6mc3q
|
||||
22
vfx/ExplosionParticle.gd
Normal file
22
vfx/ExplosionParticle.gd
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
class_name ExplosionParticle
|
||||
extends Node2D
|
||||
|
||||
# One debris chunk / energy streak inside an explosion. Draws its fixed
|
||||
# local-space shape exactly once (like Ship.gd/Asteroid.gd draw once and
|
||||
# animate via node properties), then ExplosionVFXBase drives it every frame
|
||||
# purely through `position` and `rotation` - no further redraw needed.
|
||||
#
|
||||
# The shape itself is supplied by the owning VFX class via a Callable rather
|
||||
# than being known here, since different explosion styles (glowing streak
|
||||
# vs. rock-chunk polygon) draw completely differently; this class only owns
|
||||
# the "draw once, then just move" mechanics.
|
||||
|
||||
var draw_callable: Callable
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_callable.call(self)
|
||||
1
vfx/ExplosionParticle.gd.uid
Normal file
1
vfx/ExplosionParticle.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dm2m305hlrv6l
|
||||
82
vfx/ExplosionVFXBase.gd
Normal file
82
vfx/ExplosionVFXBase.gd
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
class_name ExplosionVFXBase
|
||||
extends Node2D
|
||||
|
||||
# Shared particle motion/fade/lifetime logic for all explosion VFX types.
|
||||
# Subclasses fix their own visual style and color as facts about
|
||||
# themselves (see LightBurstVFX/AsteroidExplosionVFX), not as parameters
|
||||
# passed in by whatever destroyed the object - so "ships always explode
|
||||
# blue" is guaranteed by the class hierarchy, not by remembering to pass
|
||||
# the right Color() at each call site.
|
||||
|
||||
@export var particle_count: int = 12
|
||||
@export var lifetime: float = 0.5
|
||||
@export var min_speed: float = 60.0
|
||||
@export var max_speed: float = 220.0
|
||||
|
||||
var age: float = 0.0
|
||||
var particles: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
randomize()
|
||||
for i in range(particle_count):
|
||||
var angle: float = randf() * TAU
|
||||
var speed: float = randf_range(min_speed, max_speed)
|
||||
var p: Dictionary = {
|
||||
"pos": Vector2.ZERO,
|
||||
"vel": Vector2(cos(angle), sin(angle)) * speed,
|
||||
"rot": randf() * TAU,
|
||||
"rot_speed": randf_range(-6.0, 6.0),
|
||||
"size": _random_particle_size(),
|
||||
}
|
||||
# Each particle is its own child CanvasItem, drawn once at its own
|
||||
# fixed local shape (subclass decides the shape via
|
||||
# _configure_particle). From here on the "animation" is just this
|
||||
# child's position/rotation, exactly like Ship.gd/Asteroid.gd - no
|
||||
# redraw, no re-tessellating four polylines per particle per frame.
|
||||
var visual: ExplosionParticle = ExplosionParticle.new()
|
||||
_configure_particle(visual, p)
|
||||
if _particle_spins():
|
||||
visual.rotation = p["rot"]
|
||||
add_child(visual)
|
||||
p["visual"] = visual
|
||||
particles.append(p)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
age += delta
|
||||
if age >= lifetime:
|
||||
queue_free()
|
||||
return
|
||||
# CanvasItem modulate is inherited down the tree, so setting it once
|
||||
# here fades every particle's already-drawn shape for free - no need
|
||||
# to recompute or re-draw a single one of them.
|
||||
modulate.a = clamp(1.0 - (age / lifetime), 0.0, 1.0)
|
||||
for p in particles:
|
||||
p["pos"] += p["vel"] * delta
|
||||
p["vel"] *= 0.9
|
||||
p["rot"] += p["rot_speed"] * delta
|
||||
var visual: ExplosionParticle = p["visual"]
|
||||
visual.position = p["pos"]
|
||||
if _particle_spins():
|
||||
visual.rotation = p["rot"]
|
||||
|
||||
|
||||
func _random_particle_size() -> float:
|
||||
return randf_range(10.0, 20.0)
|
||||
|
||||
|
||||
# Called once per particle at spawn time. Subclasses set visual.draw_callable
|
||||
# to a closure that draws their own fixed local shape (a streak, a rock
|
||||
# chunk, ...) - it must not depend on `p` beyond values fixed at spawn (size,
|
||||
# initial velocity direction), since it never runs again.
|
||||
func _configure_particle(_visual: ExplosionParticle, _p: Dictionary) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# True for particle styles whose shape itself spins independently of its
|
||||
# direction of travel (tumbling debris). Energy streaks are drawn already
|
||||
# aligned to their (fixed) velocity direction, so giving them this unrelated
|
||||
# per-particle spin as well would visibly rotate them off their travel line.
|
||||
func _particle_spins() -> bool:
|
||||
return false
|
||||
1
vfx/ExplosionVFXBase.gd.uid
Normal file
1
vfx/ExplosionVFXBase.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cpb6vf3j6og6r
|
||||
19
vfx/LightBurstVFX.gd
Normal file
19
vfx/LightBurstVFX.gd
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class_name LightBurstVFX
|
||||
extends ExplosionVFXBase
|
||||
|
||||
# Shared radiating-neon-streak rendering for energy-based objects (player
|
||||
# ships, enemies) - concrete subclasses only need to fix their own color.
|
||||
|
||||
var color: Color = Color(1, 1, 1)
|
||||
|
||||
|
||||
func _configure_particle(visual: ExplosionParticle, p: Dictionary) -> void:
|
||||
# Direction only ever scales (vel *= 0.9 each frame in the base class),
|
||||
# it never turns - so the streak's local orientation is safe to fix
|
||||
# once here rather than recomputed from vel every frame.
|
||||
var vel: Vector2 = p["vel"]
|
||||
var dir: Vector2 = vel.normalized() if vel.length() > 0.01 else Vector2.RIGHT
|
||||
var half: Vector2 = dir * p["size"] * 0.5
|
||||
var line_color: Color = color
|
||||
visual.draw_callable = func(item: CanvasItem) -> void:
|
||||
NeonDraw.draw_glowing_line(item, -half, half, line_color, 1.0, 0.6)
|
||||
1
vfx/LightBurstVFX.gd.uid
Normal file
1
vfx/LightBurstVFX.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b4awmav5ftmua
|
||||
52
vfx/NeonDraw.gd
Normal file
52
vfx/NeonDraw.gd
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
class_name NeonDraw
|
||||
extends RefCounted
|
||||
|
||||
# Cheap "poor man's glow": layered translucent strokes of increasing width
|
||||
# and decreasing opacity, topped with a bright core line. No solid fill -
|
||||
# gives ships/enemies/shots a light-made, neon/laser-sabre look without
|
||||
# needing an HDR bloom pipeline.
|
||||
#
|
||||
# The core colour is a parameter rather than always white: the effect is
|
||||
# built for a dark background, and a near-white core all but disappears
|
||||
# against a bright one (the lunar surface, whose colours are sampled from a
|
||||
# real NASA photograph, is a light grey). Callers drawing onto a bright
|
||||
# backdrop pass a dark core instead, which reads as an inked outline while
|
||||
# keeping the same layered-glow construction.
|
||||
|
||||
const DEFAULT_CORE: Color = Color(1.0, 1.0, 1.0)
|
||||
|
||||
|
||||
static func draw_glowing_polygon(item: CanvasItem, points: PackedVector2Array, glow_color: Color, pulse: float = 1.0, core_color: Color = DEFAULT_CORE, width_scale: float = 1.0) -> void:
|
||||
var closed: PackedVector2Array = points.duplicate()
|
||||
closed.append(points[0])
|
||||
_draw_layers(item, closed, glow_color, pulse, width_scale, core_color)
|
||||
|
||||
|
||||
static func draw_glowing_line(item: CanvasItem, from: Vector2, to: Vector2, glow_color: Color, pulse: float = 1.0, width_scale: float = 1.0, core_color: Color = DEFAULT_CORE) -> void:
|
||||
_draw_layers(item, PackedVector2Array([from, to]), glow_color, pulse, width_scale, core_color)
|
||||
|
||||
|
||||
static func draw_glowing_circle(item: CanvasItem, center: Vector2, radius: float,
|
||||
glow_color: Color, pulse: float = 1.0,
|
||||
core_color: Color = DEFAULT_CORE) -> void:
|
||||
var points := PackedVector2Array()
|
||||
for i in range(24):
|
||||
var angle := TAU * float(i) / 24.0
|
||||
points.append(center + Vector2(cos(angle), sin(angle)) * radius)
|
||||
draw_glowing_polygon(item, points, glow_color, pulse, core_color)
|
||||
|
||||
|
||||
# An open run of points - unlike draw_glowing_polygon, the ends are NOT
|
||||
# joined. For arcs, cradle ribs and other shapes that are deliberately open.
|
||||
static func draw_glowing_path(item: CanvasItem, points: PackedVector2Array, glow_color: Color, pulse: float = 1.0, width_scale: float = 1.0, core_color: Color = DEFAULT_CORE) -> void:
|
||||
if points.size() < 2:
|
||||
return
|
||||
_draw_layers(item, points, glow_color, pulse, width_scale, core_color)
|
||||
|
||||
|
||||
static func _draw_layers(item: CanvasItem, poly: PackedVector2Array, glow_color: Color, pulse: float, width_scale: float, core_color: Color) -> void:
|
||||
var c: Color = glow_color
|
||||
item.draw_polyline(poly, Color(c.r, c.g, c.b, 0.12 * pulse), 18.0 * width_scale, true)
|
||||
item.draw_polyline(poly, Color(c.r, c.g, c.b, 0.28 * pulse), 10.0 * width_scale, true)
|
||||
item.draw_polyline(poly, Color(c.r, c.g, c.b, 0.60 * pulse), 5.0 * width_scale, true)
|
||||
item.draw_polyline(poly, Color(core_color.r, core_color.g, core_color.b, 0.95 * pulse), 1.8 * width_scale, true)
|
||||
1
vfx/NeonDraw.gd.uid
Normal file
1
vfx/NeonDraw.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d1rylxo301xin
|
||||
47
vfx/NeonPiece.gd
Normal file
47
vfx/NeonPiece.gd
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
class_name NeonPiece
|
||||
extends Node2D
|
||||
|
||||
# One movable piece of neon art, rasterised ONCE and thereafter animated
|
||||
# only through node properties.
|
||||
#
|
||||
# Godot re-runs _draw() solely when a redraw is requested; changing
|
||||
# position/rotation/scale/modulate/visible reuses the cached geometry
|
||||
# instead. Since every NeonDraw call expands into four antialiased
|
||||
# polylines, redrawing a shape each frame just to move or fade it is the
|
||||
# expensive way to do the cheapest kind of animation. Ship.gd and
|
||||
# Asteroid.gd already work this way - this is the same idea packaged so a
|
||||
# building can be assembled out of independently movable parts.
|
||||
#
|
||||
# Geometry is defined in local space around the piece's own origin, so the
|
||||
# owner can move, spin, scale and fade it without touching the shape.
|
||||
|
||||
var polygons: Array = []
|
||||
var lines: Array = []
|
||||
var glow_color: Color = Color.WHITE
|
||||
var core_color: Color = NeonDraw.DEFAULT_CORE
|
||||
var pulse: float = 1.0
|
||||
|
||||
|
||||
static func create(parent: Node, at: Vector2, colour: Color, alpha: float = 1.0) -> NeonPiece:
|
||||
var piece: NeonPiece = NeonPiece.new()
|
||||
piece.position = at
|
||||
piece.glow_color = colour
|
||||
piece.modulate.a = alpha
|
||||
parent.add_child(piece)
|
||||
return piece
|
||||
|
||||
|
||||
func add_polygon(points: PackedVector2Array, piece_pulse: float = 1.0) -> void:
|
||||
polygons.append({"points": points, "pulse": piece_pulse})
|
||||
|
||||
|
||||
func add_line(from: Vector2, to: Vector2, width: float, piece_pulse: float = 1.0) -> void:
|
||||
lines.append({"from": from, "to": to, "width": width, "pulse": piece_pulse})
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
for poly in polygons:
|
||||
NeonDraw.draw_glowing_polygon(self, poly.points, glow_color, poly.pulse * pulse, core_color)
|
||||
for line in lines:
|
||||
NeonDraw.draw_glowing_line(self, line.from, line.to, glow_color,
|
||||
line.pulse * pulse, line.width, core_color)
|
||||
1
vfx/NeonPiece.gd.uid
Normal file
1
vfx/NeonPiece.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b8r2lwqg3syw2
|
||||
101
vfx/ResourcePickupVFX.gd
Normal file
101
vfx/ResourcePickupVFX.gd
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
extends Node2D
|
||||
|
||||
# Forgejo #79: a small "+N" with the resource's own mark, rising and fading
|
||||
# where the fleet picked something up.
|
||||
#
|
||||
# Collecting used to be entirely silent - a rock vanished and a number in the
|
||||
# corner of the screen changed. Which number, and by how much, was left for
|
||||
# the player to notice on their own. This says it at the point it happened,
|
||||
# in the resource's own colour, and gets out of the way.
|
||||
#
|
||||
# Built from the SAME glyph scene the resource tables and transfer rows use
|
||||
# (ResourceDefinition.glyph_scene), so a new material added to the catalog
|
||||
# gets a correct pickup mark with no work here - and can never end up with
|
||||
# one mark in the HUD and a different one in the world.
|
||||
|
||||
const RESOURCE_DEFINITIONS = preload("res://resources/ResourceDefinitions.gd")
|
||||
# Deliberately NO class_name, and the constructor below loads this file by
|
||||
# path rather than naming the class. A global class-name identifier - even a
|
||||
# script's own, inside its own static function - needs the class cache already
|
||||
# warm, and it is not when this is reached indirectly through a preview scene
|
||||
# loading Main.tscn. That failed to compile outright, taking Rock and the gas
|
||||
# vents down with it. Callers preload the path too; see Rock.gd.
|
||||
const SELF_PATH: String = "res://vfx/ResourcePickupVFX.gd"
|
||||
|
||||
const RISE_DISTANCE: float = 46.0
|
||||
const DURATION: float = 0.85
|
||||
const GLYPH_SIZE: float = 22.0
|
||||
const DIGIT_HEIGHT: float = 20.0
|
||||
# Held fully opaque for the first part of its life, so the eye has time to
|
||||
# catch it before it starts going.
|
||||
const FADE_START: float = 0.45
|
||||
|
||||
var age: float = 0.0
|
||||
var colour: Color = Color.WHITE
|
||||
var start_y: float = 0.0
|
||||
|
||||
|
||||
static func spawn(parent: Node, world_position: Vector2, resource_type: String,
|
||||
amount: int) -> void:
|
||||
if amount <= 0:
|
||||
return
|
||||
var vfx: Node2D = (load(SELF_PATH) as GDScript).new()
|
||||
vfx.position = world_position
|
||||
vfx.build(resource_type, amount)
|
||||
parent.add_child(vfx)
|
||||
|
||||
|
||||
func build(resource_type: String, amount: int) -> void:
|
||||
var definition: Resource = RESOURCE_DEFINITIONS.get_definition(resource_type)
|
||||
colour = definition.color if definition != null else Color.WHITE
|
||||
|
||||
if definition != null and definition.glyph_scene != null:
|
||||
var glyph: Control = definition.glyph_scene.instantiate()
|
||||
glyph.colour = definition.color
|
||||
# Anchors first. The glyph scenes are authored as full-rect Controls
|
||||
# for the panels that host them, and Godot recomputes the size of any
|
||||
# Control with non-equal opposite anchors right after _ready() - so
|
||||
# assigning size without this is overwritten, with a warning, the
|
||||
# moment it enters the tree.
|
||||
glyph.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
glyph.size = Vector2(GLYPH_SIZE, GLYPH_SIZE)
|
||||
glyph.position = Vector2(-2.0, -GLYPH_SIZE * 0.5)
|
||||
add_child(glyph)
|
||||
|
||||
# show_icon off: the resource's own glyph is already beside it, and
|
||||
# NeonNumber's built-in mark is the ore hexagon whatever the amount is.
|
||||
var number := NeonNumber.new()
|
||||
number.show_icon = false
|
||||
number.amount = amount
|
||||
number.digit_height = DIGIT_HEIGHT
|
||||
number.glow_color = colour
|
||||
number.core_color = colour.darkened(0.8)
|
||||
number.size = Vector2(60.0, DIGIT_HEIGHT)
|
||||
number.position = Vector2(GLYPH_SIZE + 4.0, -DIGIT_HEIGHT * 0.5)
|
||||
add_child(number)
|
||||
|
||||
start_y = position.y
|
||||
z_index = 40
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
age += delta
|
||||
var t: float = age / DURATION
|
||||
if t >= 1.0:
|
||||
queue_free()
|
||||
return
|
||||
# Eases out: quick off the mark, drifting by the end.
|
||||
position.y = start_y - RISE_DISTANCE * (1.0 - pow(1.0 - t, 2.0))
|
||||
modulate.a = 1.0 if t < FADE_START else 1.0 - (t - FADE_START) / (1.0 - FADE_START)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# The plus sign, drawn here rather than as a third child - it is two
|
||||
# strokes and giving it its own node would cost more than it saves.
|
||||
var centre := Vector2(-12.0, 0.0)
|
||||
var arm: float = 5.0
|
||||
NeonDraw.draw_glowing_line(self, centre - Vector2(arm, 0.0), centre + Vector2(arm, 0.0),
|
||||
colour, 1.0, 0.5, colour.darkened(0.8))
|
||||
NeonDraw.draw_glowing_line(self, centre - Vector2(0.0, arm), centre + Vector2(0.0, arm),
|
||||
colour, 1.0, 0.5, colour.darkened(0.8))
|
||||
1
vfx/ResourcePickupVFX.gd.uid
Normal file
1
vfx/ResourcePickupVFX.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://byysi5a0ssymi
|
||||
12
vfx/ShipExplosionVFX.gd
Normal file
12
vfx/ShipExplosionVFX.gd
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class_name ShipExplosionVFX
|
||||
extends LightBurstVFX
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
color = Color(0.25, 0.85, 1.0)
|
||||
|
||||
|
||||
static func spawn(parent: Node, world_position: Vector2) -> void:
|
||||
var vfx: ShipExplosionVFX = ShipExplosionVFX.new()
|
||||
vfx.position = world_position
|
||||
parent.add_child(vfx)
|
||||
1
vfx/ShipExplosionVFX.gd.uid
Normal file
1
vfx/ShipExplosionVFX.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c20i5a56ib4pw
|
||||
Loading…
Reference in a new issue