Add a main menu scene (#47)

MainMenu.tscn is now project.godot's entry point: Play / Map Editor
(disabled, explains itself - #48 wires it up) / Quit (hidden in web
builds), styled like the rest of the game's menus and reusing the
starfield shader for its background.

Play calls GameState.reset_new_game(), so a second playthrough in the
same process starts fresh instead of inheriting whatever the previous
run left in the autoload - existing tests still load Main.tscn
directly and are unaffected.

Part of the #40 conquest-mechanic breakdown.
This commit is contained in:
Loïc Wisniewski 2026-08-15 14:43:46 +02:00
commit a1fb239a50
7 changed files with 297 additions and 1 deletions

95
tests/test_main_menu.gd Normal file
View file

@ -0,0 +1,95 @@
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_main_menu.gd
# Verifies:
# 1. the menu loads with Play/Map Editor/Quit, the editor button starts
# disabled and explains itself instead of silently doing nothing
# 2. Quit's visibility follows OS.has_feature("web") (this platform is
# not a web export, so this run exercises the "visible" branch; the
# condition itself is what's under test, not the concrete boolean)
# 3. Play resets GameState to a fresh game AND reaches Main.tscn
var menu: Node
var game_state: Node
var elapsed: float = 0.0
var stage: int = 0
func _initialize() -> void:
game_state = root.get_node("GameState")
menu = load("res://MainMenu.tscn").instantiate()
root.add_child(menu)
current_scene = menu
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
# A scene added to the tree inside _initialize() has not had its
# _ready() flushed yet - see test_planet_setups.gd for the same
# hazard - so the menu's @onready buttons aren't valid until a
# frame has actually processed.
if elapsed < 0.1:
return false
if menu.editor_button.enabled:
return _fail("map editor button is not disabled ahead of #48")
if menu.quit_button.visible != (not OS.has_feature("web")):
return _fail("quit button visibility did not follow OS.has_feature('web')")
_press(menu.editor_button)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.1:
return false
if "coming soon" not in menu.status_label.text.to_lower():
return _fail("disabled editor button press did not explain itself")
if current_scene != menu:
return _fail("pressing the disabled editor button changed scenes")
# Dirty the game state exactly like an in-progress run would,
# so a genuine reset is distinguishable from a coincidental match.
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 999)
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
game_state.surviving_ship_count = 1
game_state.planet_capture["chunk:1:1"] = {"captured": [true], "conquered": true}
_press(menu.play_button)
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.3:
return false
var on_main: bool = current_scene != null and current_scene.name == "Main"
var reset_ok: bool = game_state.rocks == 500 and game_state.buildings.is_empty() \
and game_state.surviving_ship_count == GameState.BASE_FLEET_SIZE \
and game_state.planet_capture.is_empty()
print("on_main=%s reset_ok=%s rocks=%d buildings=%d ship_count=%d" % [
on_main, reset_ok, game_state.rocks, game_state.buildings.size(),
game_state.surviving_ship_count
])
if not on_main or not reset_ok:
return _fail("Play did not reset to a fresh game before reaching Main.tscn")
print("TEST_MAIN_MENU: PASS")
quit(0)
return true
return false
func _press(button: IconButton) -> void:
var down: InputEventScreenTouch = InputEventScreenTouch.new()
down.pressed = true
button._gui_input(down)
var up: InputEventScreenTouch = InputEventScreenTouch.new()
up.pressed = false
button._gui_input(up)
func _fail(message: String) -> bool:
print("TEST_MAIN_MENU: FAIL (%s)" % message)
quit(1)
return true

View file

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