Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the hauling are #19. Placeable, costed at 30, gated behind a new "Freight Handling" research node hanging off Logistics, with its own info popup, build-menu glyph and map marker. Drawn as an octagonal dock ring with berth arms and a squared-off freight core: deliberately radial-but-flat so it does not read as a small mothership, which is the only structure with swept arms. Its stationed transports are NeonPiece children of one rotating holder, per #10 - the geometry is rasterised once and only the holder's rotation changes, so the traffic costs no redraws. Building._draw()'s match has a catch-all that renders anything unrecognised as a ship crafter, so a missing case is silent and looks almost right. The test checks for the hub's own _hub_traffic holder rather than trusting that something plausible appeared - which caught exactly that: a _ready() patch had silently not applied and the hub was rendering through the crafter branch. The build menu grew a third row. Every button, label and price moved: the old row occupied 332px against a 372px pitch, and three of those do not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px price digits) at a 290px pitch, ending at y=1176. Rendered and looked at - the first attempt tightened the pitch alone and left every price overlapping the row beneath it, which no headless test would have seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ff162faef5
commit
823828f3b3
10 changed files with 400 additions and 52 deletions
20
tests/_build_menu_preview.gd
Normal file
20
tests/_build_menu_preview.gd
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test. Opens the build menu itself, so the three-row layout
|
||||
# added for the Transport Hub (#18) can be checked for fit on the real phone
|
||||
# viewport - every button, label and price moved, and a bad offset is
|
||||
# invisible to a headless test.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_build_menu_preview.tscn /tmp/menu.png 20 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.buildings.clear()
|
||||
GameState.extractors.clear()
|
||||
GameState.rocks = 500
|
||||
# Everything researched, so nothing is greyed out and the whole menu is
|
||||
# legible at full strength.
|
||||
GameState.unlocked = ["shipyards", "logistics", "freight", "coordination"]
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("UI/BuildMenu").visible = true
|
||||
6
tests/_build_menu_preview.tscn
Normal file
6
tests/_build_menu_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_build_menu_preview.gd" id="1"]
|
||||
|
||||
[node name="BuildMenuPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -13,8 +13,9 @@ func _ready() -> void:
|
|||
GameState.buildings.clear()
|
||||
GameState.extractors.clear()
|
||||
GameState.reset_formation()
|
||||
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(-230.0, 0.0))
|
||||
GameState.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(240.0, 0.0))
|
||||
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(-330.0, 0.0))
|
||||
GameState.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(140.0, 0.0))
|
||||
GameState.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(480.0, 0.0))
|
||||
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
|
|
@ -24,4 +25,4 @@ func _ready() -> void:
|
|||
# far enough to see them whole.
|
||||
main.get_node("Fleet").move_to(Vector2(0.0, 210.0))
|
||||
var cam: Camera2D = main.get_node("Camera2D")
|
||||
cam.zoom = Vector2(1.35, 1.35)
|
||||
cam.zoom = Vector2(1.0, 1.0)
|
||||
|
|
|
|||
157
tests/test_transport_hub_building.gd
Normal file
157
tests/test_transport_hub_building.gd
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_transport_hub_building.gd
|
||||
# Forgejo #18 (part 5 of #12): the Transport Hub is a placeable building of
|
||||
# its own. Only the building here - the vessels that do the hauling are #19.
|
||||
#
|
||||
# Verifies:
|
||||
# 1. it is gated behind its research node, like every other structure
|
||||
# 2. researching it makes it buildable, and placing it charges its cost
|
||||
# 3. it is drawn as ITSELF, not as the crafter - Building._draw()'s match
|
||||
# has a catch-all that renders anything unrecognised as a ship crafter,
|
||||
# so a missing case is silent and looks almost right
|
||||
# 4. its geometry is rasterised ONCE however long it animates (#10), and
|
||||
# its stationed transports really do move
|
||||
# 5. it has its own build-menu icon and its own map marker rather than
|
||||
# borrowing another building's - the menu was once unreadable because
|
||||
# two buildings shared a glyph
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
var field: Node
|
||||
var controller: Node
|
||||
var map_content: Node2D
|
||||
var hub: Node2D
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var spin_at_start: float = 0.0
|
||||
|
||||
const HUB_AT: Vector2 = Vector2(700.0, 0.0)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.reset_progression()
|
||||
game_state.rocks = 500
|
||||
game_state.fleet_cargo = 0
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
field = scene.get_node("BuildingField")
|
||||
controller = scene.get_node("BuildController")
|
||||
map_content = scene.get_node("UI/WorldMap/MapContent")
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
scene.get_node("AsteroidField").set_process(false)
|
||||
scene.get_node("EnemySpawner").set_process(false)
|
||||
|
||||
# --- 1. locked until researched ------------------------------
|
||||
if game_state.is_building_unlocked(GameState.BUILDING_TRANSPORT_HUB):
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (buildable with no research at all)")
|
||||
quit(1)
|
||||
return true
|
||||
var gate: String = game_state.research_for_building(GameState.BUILDING_TRANSPORT_HUB)
|
||||
if gate == "":
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (no research node gates it)")
|
||||
quit(1)
|
||||
return true
|
||||
print("locked behind research: '%s'" % gate)
|
||||
|
||||
# --- 5. its own icon, distinct from every other building -----
|
||||
var icons: Dictionary = {}
|
||||
for type in [GameState.BUILDING_MOTHERSHIP, GameState.BUILDING_SHIP_CRAFTER,
|
||||
GameState.BUILDING_COMMAND_CENTER, GameState.BUILDING_TRANSPORT_HUB]:
|
||||
var icon: int = controller._action_icon_for(type)
|
||||
if icons.has(icon):
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (%s and %s share an icon)" % [type, icons[icon]])
|
||||
quit(1)
|
||||
return true
|
||||
icons[icon] = type
|
||||
print("action icons distinct across %d buildings" % icons.size())
|
||||
|
||||
# --- 2. research it, then build it ---------------------------
|
||||
game_state.unlocked = ["shipyards", "logistics", "freight"]
|
||||
if not game_state.is_building_unlocked(GameState.BUILDING_TRANSPORT_HUB):
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (still locked after researching '%s')" % gate)
|
||||
quit(1)
|
||||
return true
|
||||
var before: int = game_state.rocks
|
||||
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
||||
game_state.spend(controller.cost_of(GameState.BUILDING_TRANSPORT_HUB))
|
||||
if game_state.rocks != before - GameState.TRANSPORT_HUB_COST:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (cost %d charged, expected %d)" % [
|
||||
before - game_state.rocks, GameState.TRANSPORT_HUB_COST
|
||||
])
|
||||
quit(1)
|
||||
return true
|
||||
print("researched and placed for %d ore" % GameState.TRANSPORT_HUB_COST)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
for b in field.spawned:
|
||||
if is_instance_valid(b) and b.building_type == GameState.BUILDING_TRANSPORT_HUB:
|
||||
hub = b
|
||||
if hub == null:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
# --- 3. drawn as itself, not via the crafter catch-all -------
|
||||
# _hub_traffic only exists if _ready() took the transport-hub
|
||||
# branch; a missing case falls through to the crafter and would
|
||||
# leave it null while still rendering something plausible.
|
||||
if hub._hub_traffic == null:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (rendered through the crafter catch-all, not as a hub)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
# --- 5. its own map marker ----------------------------------
|
||||
var markers: Array = map_content.building_markers()
|
||||
var found: bool = false
|
||||
for m in markers:
|
||||
if m.type == GameState.BUILDING_TRANSPORT_HUB:
|
||||
found = true
|
||||
if m.color == map_content.CRAFTER_MARKER_COLOR \
|
||||
or m.color == map_content.MOTHERSHIP_MARKER_COLOR \
|
||||
or m.color == map_content.COMMAND_MARKER_COLOR:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (map marker reuses another building's colour)")
|
||||
quit(1)
|
||||
return true
|
||||
if not found:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (no map marker - see #11)")
|
||||
quit(1)
|
||||
return true
|
||||
print("has its own map marker")
|
||||
|
||||
spin_at_start = hub._hub_traffic.rotation
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
# --- 4. animates without ever redrawing (#10) ----------------
|
||||
if elapsed < 2.0:
|
||||
return false
|
||||
print("rasterisations after %.1fs: %d" % [elapsed, hub.draw_count])
|
||||
if hub.draw_count != 1:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (redrew its geometry %d times to animate)" % hub.draw_count)
|
||||
quit(1)
|
||||
return true
|
||||
var spun: float = absf(hub._hub_traffic.rotation - spin_at_start)
|
||||
print("stationed transports turned %.3f rad" % spun)
|
||||
if spun < 0.05:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (nothing moved - a frozen hub trivially passes the redraw check)")
|
||||
quit(1)
|
||||
return true
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
Loading…
Reference in a new issue