Show buildings on the world map, and extractors on their planets
Forgejo #11: buildings the player has placed now appear on the world map. They are placed freely in space rather than tied to a chunk, so they get their own pass in WorldMapContent._draw() rather than riding along with the chunk markers. Each type has its own shape and colour, mirroring Building.gd's constants so a marker reads as the same building you built. Forgejo #9: a planet with an extractor built on it now shows it, in both places you look at a planet - an amber derrick on its outline in space, and a matching badge on its world-map marker. The extractor lives on PlanetSurface.tscn, a scene you are not looking at from space, so an already-mined planet was otherwise indistinguishable from an untouched one until you landed on it. The positioning steps are factored out (building_markers(), chunk_has_extractor_badge(), extractor_derrick_tip()) so headless tests can assert on them - --headless has no GPU and draws no pixels, so there is nothing to check in _draw() itself. Both features were also rendered for real via tests/screenshot.sh and looked at. extractor_derrick_tip() exists because the first version of the "derrick is not buried inside the planet" check measured the furthest of all four derrick points, which passes a derrick pointing straight into the ground: the base corners sit a half-width off the rim vertex and clear the outline on their own. Mutation-testing caught it. screenshot.sh now clears a stale /tmp/.X99-lock. A run killed mid-screenshot leaves it behind and Xvfb then refuses :99, which resurfaces later as Godot's misleading "X11 Display is not available". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b641fa5891
commit
8dd5d7b78f
13 changed files with 472 additions and 0 deletions
45
tests/_marker_preview.gd
Normal file
45
tests/_marker_preview.gd
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test (underscore prefix keeps it out of run_tests.sh).
|
||||
# Renders the world map with buildings placed and an extractor built, so the
|
||||
# issue #11 building markers and the issue #9 extractor badge can actually be
|
||||
# LOOKED at. --headless draws no pixels, so no headless test can tell whether
|
||||
# a marker is visible - see tests/screenshot.sh.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_marker_preview.tscn /tmp/map.png 20 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.buildings.clear()
|
||||
GameState.extractors.clear()
|
||||
GameState.explored_chunks.clear()
|
||||
|
||||
# Near the origin, where the fleet starts, so everything lands on screen.
|
||||
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(900.0, -700.0))
|
||||
GameState.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(-1100.0, 300.0))
|
||||
GameState.add_building(GameState.BUILDING_COMMAND_CENTER, Vector2(400.0, 1200.0))
|
||||
|
||||
# Reveal a patch of map, and put an extractor on whichever planet is
|
||||
# nearest the origin so its badge is in frame too.
|
||||
var best: Vector2i = Vector2i.ZERO
|
||||
var best_distance: int = 1 << 30
|
||||
for x in range(-6, 7):
|
||||
for y in range(-6, 7):
|
||||
var chunk: Vector2i = Vector2i(x, y)
|
||||
GameState.explored_chunks[chunk] = true
|
||||
if WorldGen.has_planet_at_chunk(chunk):
|
||||
var d: int = absi(x) + absi(y)
|
||||
if d < best_distance:
|
||||
best_distance = d
|
||||
best = chunk
|
||||
if best_distance < (1 << 30):
|
||||
var data: Dictionary = WorldGen.planet_data_at_chunk(best)
|
||||
GameState.add_extractor(data.name, WorldGen.chunk_origin(best) + data.local_pos)
|
||||
print("extractor on '%s' at chunk %s" % [data.name, best])
|
||||
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
# open_map(), not `visible = true` - opening also recentres the view on
|
||||
# the fleet, and without that the map is drawn from its own origin in the
|
||||
# corner of the screen with everything near the fleet cropped off.
|
||||
main.get_node("UI/WorldMap").open_map()
|
||||
1
tests/_marker_preview.gd.uid
Normal file
1
tests/_marker_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://blbow3jq36fdw
|
||||
6
tests/_marker_preview.tscn
Normal file
6
tests/_marker_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_marker_preview.gd" id="1"]
|
||||
|
||||
[node name="MarkerPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
25
tests/_planet_derrick_preview.gd
Normal file
25
tests/_planet_derrick_preview.gd
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test. Two planets side by side in SPACE - the left one has
|
||||
# an extractor built on it, the right one does not - so the issue #9 derrick
|
||||
# can be compared against a plain planet and judged as actually visible.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_planet_derrick_preview.tscn /tmp/derrick.png 15 1280x720
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.extractors.clear()
|
||||
GameState.buildings.clear()
|
||||
GameState.add_extractor("Mined World", Vector2.ZERO)
|
||||
|
||||
_spawn("Mined World", Vector2(380.0, 380.0), 12345)
|
||||
_spawn("Untouched World", Vector2(900.0, 380.0), 4242)
|
||||
|
||||
|
||||
func _spawn(planet_name: String, at: Vector2, shape_seed: int) -> void:
|
||||
var planet: Node2D = load("res://Planet.tscn").instantiate()
|
||||
planet.planet_name = planet_name
|
||||
planet.shape_seed = shape_seed
|
||||
planet.position = at
|
||||
add_child(planet)
|
||||
print("%s: derrick=%s" % [planet_name, planet.shows_extractor])
|
||||
1
tests/_planet_derrick_preview.gd.uid
Normal file
1
tests/_planet_derrick_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dl4abkye58bd6
|
||||
6
tests/_planet_derrick_preview.tscn
Normal file
6
tests/_planet_derrick_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_planet_derrick_preview.gd" id="1"]
|
||||
|
||||
[node name="PlanetDerrickPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -28,6 +28,13 @@ WAIT_FRAMES="${3:-60}"
|
|||
RESOLUTION="${4:-1280x720}"
|
||||
|
||||
proot-distro login ubuntu -- bash -c "
|
||||
# A killed run (or a session that ended mid-screenshot) leaves /tmp/.X99-lock
|
||||
# behind, and Xvfb then refuses to claim :99 - which surfaces much later and
|
||||
# far less obviously as Godot's 'X11 Display is not available'. Nothing else
|
||||
# uses :99 here, so a lock with no live Xvfb behind it is always stale.
|
||||
if ! pgrep -x Xvfb >/dev/null; then
|
||||
rm -f /tmp/.X99-lock /tmp/.X11-unix/X99
|
||||
fi
|
||||
Xvfb :99 -screen 0 ${RESOLUTION}x24 &
|
||||
XVFB_PID=\$!
|
||||
sleep 2
|
||||
|
|
|
|||
99
tests/test_map_building_markers.gd
Normal file
99
tests/test_map_building_markers.gd
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_map_building_markers.gd
|
||||
# Forgejo issue #11: a building the player has placed must appear on the
|
||||
# world map. Without it the map showed explored chunks, planets and the
|
||||
# fleet, but nothing the player had actually built - so there was no way to
|
||||
# find your own base again once you had flown away from it.
|
||||
#
|
||||
# Buildings are not tied to a chunk (they are placed freely in space), so
|
||||
# they cannot ride along with the chunk markers and need their own pass.
|
||||
#
|
||||
# --headless has no GPU and draws no pixels, so this cannot assert on what
|
||||
# was rendered. It asserts on building_markers(), the step that turns
|
||||
# GameState.buildings into map-local marker data, which is the part that
|
||||
# _draw() then simply stamps out.
|
||||
#
|
||||
# Verifies:
|
||||
# 1. no buildings -> no markers (so a passing count is not just "always 3")
|
||||
# 2. every placed building produces exactly one marker
|
||||
# 3. markers land at the building's world position scaled into map space
|
||||
# 4. the three building types are told apart by colour, rather than all
|
||||
# being drawn as the same anonymous dot
|
||||
|
||||
var scene: Node
|
||||
var game_state: Node
|
||||
var map_content: Node2D
|
||||
var stage: int = 0
|
||||
|
||||
const MOTHERSHIP_AT: Vector2 = Vector2(2000.0, -1200.0)
|
||||
const CRAFTER_AT: Vector2 = Vector2(-800.0, 400.0)
|
||||
const COMMAND_AT: Vector2 = Vector2(150.0, 3000.0)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
map_content = scene.get_node("UI/WorldMap/MapContent")
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
match stage:
|
||||
0:
|
||||
# Nothing built yet: the map must show no building markers at all.
|
||||
var empty: Array = map_content.building_markers()
|
||||
if not empty.is_empty():
|
||||
print("TEST_MAP_BUILDING_MARKERS: FAIL (%d markers with nothing built)" % empty.size())
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
||||
game_state.add_building(GameState.BUILDING_COMMAND_CENTER, COMMAND_AT)
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
var markers: Array = map_content.building_markers()
|
||||
print("markers=%d for %d buildings" % [markers.size(), game_state.buildings.size()])
|
||||
if markers.size() != 3:
|
||||
print("TEST_MAP_BUILDING_MARKERS: FAIL (expected one marker per building)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
var scale: float = map_content.WORLD_TO_LOCAL
|
||||
var expected: Dictionary = {
|
||||
GameState.BUILDING_MOTHERSHIP: MOTHERSHIP_AT * scale,
|
||||
GameState.BUILDING_SHIP_CRAFTER: CRAFTER_AT * scale,
|
||||
GameState.BUILDING_COMMAND_CENTER: COMMAND_AT * scale,
|
||||
}
|
||||
var colours: Array = []
|
||||
for m in markers:
|
||||
if not expected.has(m.type):
|
||||
print("TEST_MAP_BUILDING_MARKERS: FAIL (unexpected marker type %s)" % m.type)
|
||||
quit(1)
|
||||
return true
|
||||
var want: Vector2 = expected[m.type]
|
||||
if m.position.distance_to(want) > 0.001:
|
||||
print("%s marker at %s, expected %s" % [m.type, m.position, want])
|
||||
print("TEST_MAP_BUILDING_MARKERS: FAIL (marker is not where the building is)")
|
||||
quit(1)
|
||||
return true
|
||||
colours.append(m.color)
|
||||
|
||||
# Three buildings drawn in one indistinguishable colour would
|
||||
# satisfy every check above and still be useless to read.
|
||||
if colours[0] == colours[1] or colours[1] == colours[2] or colours[0] == colours[2]:
|
||||
print("colours=%s" % [colours])
|
||||
print("TEST_MAP_BUILDING_MARKERS: FAIL (building types are not visually distinct)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
print("all 3 markers positioned correctly, colours distinct: %s" % [colours])
|
||||
print("TEST_MAP_BUILDING_MARKERS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
1
tests/test_map_building_markers.gd.uid
Normal file
1
tests/test_map_building_markers.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bx3bce8ix4rbg
|
||||
138
tests/test_planet_extractor_marker.gd
Normal file
138
tests/test_planet_extractor_marker.gd
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_planet_extractor_marker.gd
|
||||
# Forgejo issue #9: a planet with an extractor built on it must show that
|
||||
# something is built there. The extractor lives on PlanetSurface.tscn, a
|
||||
# scene the player is not looking at from space, so an already-mined planet
|
||||
# was indistinguishable from an untouched one - you had to land to find out.
|
||||
#
|
||||
# Covered in both places the player looks at a planet:
|
||||
# - in space, as a derrick on the planet's own outline (Planet.gd)
|
||||
# - on the world map, as a badge on the planet marker (WorldMapContent.gd)
|
||||
#
|
||||
# --headless has no GPU, so this asserts on the state and geometry that
|
||||
# _draw() consumes, not on pixels. tests/screenshot.sh covers whether it is
|
||||
# actually visible.
|
||||
#
|
||||
# Verifies:
|
||||
# 1. a planet with no extractor shows neither derrick nor badge (so a pass
|
||||
# cannot just mean "always on")
|
||||
# 2. building one turns both on, keyed by planet NAME - the same key
|
||||
# GameState.has_extractor_at uses
|
||||
# 3. the derrick sits outside the planet's outline rather than buried in
|
||||
# it, which is the difference between visible and not
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
var map_content: Node2D
|
||||
var planet_chunk: Vector2i
|
||||
var planet_name: String = ""
|
||||
var stage: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
|
||||
var found: bool = false
|
||||
for x in range(-15, 15):
|
||||
for y in range(-15, 15):
|
||||
var chunk: Vector2i = Vector2i(x, y)
|
||||
if WorldGen.has_planet_at_chunk(chunk):
|
||||
planet_chunk = chunk
|
||||
planet_name = WorldGen.planet_data_at_chunk(chunk).name
|
||||
found = true
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if not found:
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (no planet chunk to test with)")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
map_content = scene.get_node("UI/WorldMap/MapContent")
|
||||
|
||||
|
||||
func _spawn_planet() -> Node2D:
|
||||
var planet: Node2D = load("res://Planet.tscn").instantiate()
|
||||
planet.planet_name = planet_name
|
||||
planet.shape_seed = 12345
|
||||
scene.add_child(planet)
|
||||
return planet
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
match stage:
|
||||
0:
|
||||
# --- nothing built there yet --------------------------------
|
||||
var clean: Node2D = _spawn_planet()
|
||||
if clean.shows_extractor:
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (derrick on a planet with no extractor)")
|
||||
quit(1)
|
||||
return true
|
||||
if map_content.chunk_has_extractor_badge(planet_chunk):
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (map badge with no extractor)")
|
||||
quit(1)
|
||||
return true
|
||||
clean.queue_free()
|
||||
print("no extractor -> no derrick, no badge (planet '%s')" % planet_name)
|
||||
|
||||
game_state.add_extractor(planet_name, WorldGen.chunk_origin(planet_chunk))
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# --- extractor built ----------------------------------------
|
||||
var mined: Node2D = _spawn_planet()
|
||||
if not mined.shows_extractor:
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (no derrick after building an extractor)")
|
||||
quit(1)
|
||||
return true
|
||||
if not map_content.chunk_has_extractor_badge(planet_chunk):
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (no map badge after building an extractor)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
# The derrick has to clear the outline to be seen at all. Compare
|
||||
# its tip against the planet's own radius in that direction, not
|
||||
# against a constant - the outline is a lumpy seeded shape.
|
||||
var derrick: PackedVector2Array = mined.extractor_derrick_points()
|
||||
if derrick.size() < 3:
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (derrick has no shape)")
|
||||
quit(1)
|
||||
return true
|
||||
# Measure the TIP, not the whole polygon. The base corners sit a
|
||||
# half-width to either side of the rim vertex, so they are
|
||||
# sqrt(|base|^2 + w^2) from the centre and clear the rim on their
|
||||
# own - a max over every point called a derrick pointing straight
|
||||
# into the ground "visible", which is exactly the bug this check
|
||||
# exists to catch.
|
||||
var furthest: float = mined.extractor_derrick_tip().length()
|
||||
var rim: float = 0.0
|
||||
for p in mined.shape_points:
|
||||
rim = maxf(rim, p.length())
|
||||
print("derrick tip reaches %.1f, planet outline reaches %.1f" % [furthest, rim])
|
||||
if furthest <= rim:
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (derrick buried inside the planet)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
# A different planet must NOT inherit the marker: the flag is keyed
|
||||
# by name, and getting that wrong would light up every planet.
|
||||
var other: Node2D = load("res://Planet.tscn").instantiate()
|
||||
other.planet_name = planet_name + " II"
|
||||
other.shape_seed = 999
|
||||
scene.add_child(other)
|
||||
if other.shows_extractor:
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: FAIL (marker leaked onto a different planet)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
print("extractor -> derrick + badge, and only on the right planet")
|
||||
print("TEST_PLANET_EXTRACTOR_MARKER: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
1
tests/test_planet_extractor_marker.gd.uid
Normal file
1
tests/test_planet_extractor_marker.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dcr7qb1yoewcy
|
||||
Loading…
Reference in a new issue