Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
4.1 KiB
GDScript
115 lines
4.1 KiB
GDScript
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; Map Editor is enabled and
|
|
# reaches PlanetSetupEditor.tscn (#48)
|
|
# 2. there is no Quit entry in any build - a web build has nothing to quit
|
|
# to, and Android's back gesture already does it
|
|
# 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://ui/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 not menu.editor_button.enabled:
|
|
return _fail("map editor button is not enabled (#48 shipped)")
|
|
# Both development entries follow the same rule, and the rule is
|
|
# what is under test rather than the concrete boolean - this run
|
|
# is from Godot, so it exercises the "shown" branch.
|
|
var developing: bool = menu.is_developer_build()
|
|
if menu.editor_button.visible != developing or menu.editor_label.visible != developing:
|
|
return _fail("map editor entry visibility did not follow is_developer_build()")
|
|
if menu.dev_button.visible != developing or menu.dev_label.visible != developing:
|
|
return _fail("dev mode entry visibility did not follow is_developer_build()")
|
|
if menu.has_node("UI/QuitButton") or menu.has_node("UI/QuitLabel"):
|
|
return _fail("the menu still carries a Quit entry")
|
|
|
|
_press(menu.editor_button)
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
# _start awaits a frame so its status text paints before the
|
|
# blocking load, so the change takes a couple of frames.
|
|
if elapsed < 1.0:
|
|
return false
|
|
var on_editor: bool = current_scene != null and current_scene.name == "PlanetSetupEditor"
|
|
if not on_editor:
|
|
return _fail("pressing Map Editor did not reach PlanetSetupEditor.tscn")
|
|
|
|
# Return to the menu the same way the player would, then test Play.
|
|
menu = load("res://ui/MainMenu.tscn").instantiate()
|
|
root.add_child(menu)
|
|
current_scene = menu
|
|
elapsed = 0.0
|
|
stage = 2
|
|
return false
|
|
2:
|
|
if elapsed < 0.1:
|
|
return false
|
|
# 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 = 3
|
|
return false
|
|
3:
|
|
if elapsed < 1.0:
|
|
return false
|
|
var on_main: bool = current_scene != null and current_scene.name == "Main"
|
|
# A normal game now starts with NOTHING banked - the opening
|
|
# mining run is the tutorial, and 500 free ore skipped it.
|
|
var reset_ok: bool = game_state.rocks == 0 and game_state.buildings.is_empty() \
|
|
and game_state.surviving_ship_count == GameState.STARTING_SHIP_COUNT \
|
|
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
|