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 # 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"), []) # A profile is allowed to have no scenery of its own - the research lab # had none but the capture rings, and #211 took those away. The rule is # about features that EXIST, so an empty profile is skipped rather than # failed; the run-wide check below is what stops this test quietly # checking nothing at all. if features.is_empty(): print("%s: no drawing surface features of its own" \ % 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(): if checked == 0: return _fail("no profile had a single drawing feature - this test proved nothing") 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