#91: keep the gas source behind the fleet

On a gas planet the vents were drawn on top of the player. SurfaceFeatures
is added after Fleet in PlanetSurface.tscn, so at the default z_index of 0
the vent field won the tie purely by being the later sibling, and the fleet
flew under its own gas source.

Same one-line fix, for the same reason, as #50's capture rings and the
rocks on a rocky surface. It goes on the field rather than on children
because this one draws in its own _draw().

The test checks the rule instead of the node that was wrong: every drawing
feature, on every surface profile that has features, must end up behind the
fleet - 22 of them today, and any added later without editing the test.
Enemies are excluded and the test says why; they are combatants and share
the fleet's plane on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-22 23:29:21 +02:00
commit 03b1c85569
3 changed files with 142 additions and 0 deletions

View file

@ -13,6 +13,14 @@ var vents: Array[Vector2] = []
var progress: float = 0.0
var emission_phase: float = 0.0
func _ready() -> void:
# #91: the vents are ground features and must never cover the fleet.
# SurfaceFeatures is added AFTER Fleet in PlanetSurface.tscn, so at the
# default z_index of 0 the later sibling simply wins and the player flies
# UNDER their own gas source. Same fix, and the same reason, as #50's
# capture rings and the rocks on a rocky surface (Rock.gd) - the
# difference is that this field draws in its own _draw() rather than
# spawning child nodes, so the depth belongs on the field itself.
z_index = -1
var rng := RandomNumberGenerator.new()
rng.seed = hash(GameState.planet_id if GameState.planet_id != "" else GameState.planet_name)
for i in range(6): vents.append(Vector2(rng.randf_range(-800,800), rng.randf_range(-650,650)))

View file

@ -0,0 +1,133 @@
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_surface_feature_depth.gd
# Forgejo #91: on a gas planet the gas source was drawn ON TOP of the player.
#
# SurfaceFeatures is added after Fleet in PlanetSurface.tscn, so anything
# down there that keeps the default z_index of 0 wins the tie against the
# ships purely by being the later sibling. The player then flies under their
# own scenery, which reads as a rendering fault rather than as depth.
#
# Checks the RULE rather than the one node that was wrong: every feature that
# actually draws, on every surface profile that has features, must end up
# behind the fleet. A feature added later is covered without editing this.
#
# Covers:
# A. the gas giant's vent field draws behind the ships (the #91 report)
# B. the same for every other drawing feature on every profile, rocks and
# capture rings included
# C. the fleet's own ships are where the comparison says they are, so a
# future change that pushed the ships back instead cannot make A and B
# pass by moving the goalposts
const PROFILE_PLANETS: Array = ["gas_giant", "rocky", "research_lab"]
var game_state: Node
var surface: Node
var planets: Array = []
var checked: int = 0
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
# The autoload is reached by node, not by the bare `GameState` name: from
# a SceneTree script that name resolves only far enough for constants,
# and an instance member through it is a COMPILE error for the whole
# file. Every other test here does the same.
game_state = root.get_node("GameState")
planets = PROFILE_PLANETS.duplicate()
game_state.reset_progression()
game_state.surviving_ship_count = 3
_load_next()
func _load_next() -> void:
if surface != null and is_instance_valid(surface):
surface.queue_free()
surface = null
if planets.is_empty():
return
var definition_id: String = String(planets.pop_front())
game_state.planet_definition_id = definition_id
game_state.planet_name = "Depth Probe"
game_state.planet_id = "depth-probe-%s" % definition_id
surface = load("res://surface/PlanetSurface.tscn").instantiate()
surface.get_node("SurfaceController").fade_duration = 0.0
root.add_child(surface)
# z_index is RELATIVE to the parent by default, so a node's real depth is the
# running sum up the chain - reading z_index alone would call a +1 child of a
# -1 parent "behind", which it is not.
func _effective_z(node: CanvasItem) -> int:
var total: int = 0
var current: Node = node
while current is CanvasItem:
total += (current as CanvasItem).z_index
if not (current as CanvasItem).z_as_relative:
break
current = current.get_parent()
return total
func _drawing_descendants(node: Node, found: Array) -> Array:
for child in node.get_children():
# Enemies are spawned under SurfaceFeatures by the planet's setup
# markers, but they are combatants, not scenery: they share the
# fleet's plane on purpose, the same way they do in space. Only the
# ground the fight happens on has to stay behind it.
if child.is_in_group("enemies"):
continue
# has_method("_draw") is true only where a script defines one, which
# is exactly "this node puts pixels on the surface". Plain Node2D
# hosts that only spawn their children draw nothing themselves and
# are free to sit at 0 - the rocky surface's RockField is one.
if child is CanvasItem and child.has_method("_draw"):
found.append(child)
_drawing_descendants(child, found)
return found
func _process(delta: float) -> bool:
elapsed += delta
if elapsed < 0.35:
return false
elapsed = 0.0
var fleet: Node2D = surface.get_node("Fleet")
if fleet.ships.is_empty():
return _fail("no ships spawned, so there is nothing to be in front of")
var ship_z: int = _effective_z(fleet.ships[0])
if ship_z != 0:
# C. The comparison below is "behind the ships", and the ships being
# at 0 is what makes "negative" mean that.
return _fail("ships are at z=%d, not 0 - this test's premise moved" % ship_z)
var features: Array = _drawing_descendants(surface.get_node("SurfaceFeatures"), [])
if features.is_empty():
return _fail("%s has no drawing surface features to check" % game_state.planet_definition_id)
for feature in features:
var z: int = _effective_z(feature)
if z >= ship_z:
return _fail("%s on %s draws at z=%d, in front of the fleet at z=%d" % [
feature.name, game_state.planet_definition_id, z, ship_z
])
print("%s: %d drawing features, all behind the fleet" % [
game_state.planet_definition_id, features.size()
])
checked += features.size()
if planets.is_empty():
print("TEST_SURFACE_FEATURE_DEPTH: PASS (%d features checked)" % checked)
quit(0)
return true
_load_next()
return false
func _fail(message: String) -> bool:
print("TEST_SURFACE_FEATURE_DEPTH: FAIL (%s)" % message)
quit(1)
return true

View file

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