Turn dev mode into sandbox menu

This commit is contained in:
Loïc Wisniewski 2026-09-03 20:05:33 +02:00
commit c325501999
19 changed files with 651 additions and 54 deletions

View file

@ -0,0 +1,28 @@
extends Node2D
# Visual-only phone preview for the editor sandbox panel. It builds only the
# fleet and the menu; loading Main also schedules infinite-world population,
# which is wasted behind this opaque panel and painfully slow in softpipe.
#
# bash tests/screenshot.sh res://tests/_dev_menu_preview.tscn /tmp/dev_menu.png 4 720x1280
const FLEET = preload("res://vessels/Fleet.gd")
const DEV_MENU = preload("res://ui/menus/DevMenu.tscn")
func _ready() -> void:
GameState.start_dev_game()
GameState.dev_unlock_all_skills()
var fleet: Node = FLEET.new()
add_child(fleet)
fleet.spawn_ships(4, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR,
GameState.VESSEL_SHIELD,
GameState.VESSEL_GAS_BOMBER,
GameState.VESSEL_LASER,
]))
var layer := CanvasLayer.new()
add_child(layer)
var menu: Control = DEV_MENU.instantiate()
layer.add_child(menu)
menu.open_for(fleet)

View file

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

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_dev_menu_preview.gd" id="1"]
[node name="DevMenuPreview" type="Node2D"]
script = ExtResource("1")

95
tests/test_dev_menu.gd Normal file
View file

@ -0,0 +1,95 @@
extends SceneTree
# The dev button is now a doorway rather than a one-off wave shortcut. This
# exercises the real Main scene so additions and removals prove they affect
# the live fleet, not only GameState bookkeeping.
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
var state: Node
var scene: Node
var fleet: Node
var dev_button: Control
var menu: Control
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
state = root.get_node("GameState")
state.start_dev_game()
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
fleet = scene.get_node("Fleet")
dev_button = scene.get_node("UI/DevMenuButton")
menu = scene.get_node("UI/DevMenu")
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.2:
return false
if state.intro_active:
return _fail("the dev world opened inside the intro")
if fleet.ships.size() != GameState.STARTING_SHIP_COUNT:
return _fail("post-intro dev mode did not start with the normal fleet")
dev_button.button.pressed.emit()
if not menu.visible:
return _fail("the HUD dev button opened no menu")
menu.unlock_button.pressed.emit()
for skill in GameState.SKILL_TREE:
if not state.is_unlocked(String(skill.id)):
return _fail("unlock-all missed %s" % skill.id)
if state.max_fleet_size() != GameState.DEFAULT_FORMATION.size():
return _fail("unlock-all did not expose the complete fleet capacity")
for definition in VESSEL_DEFINITIONS.fleet():
menu.fleet_add_requested.emit(String(definition.definition_id))
for definition in VESSEL_DEFINITIONS.fleet():
if not fleet.has_vessel_type(String(definition.definition_id)):
return _fail("the menu could not add %s" % definition.display_name)
print("dev menu added every fleet-eligible vessel to the live fleet")
var before: int = fleet.ships.size()
menu.fleet_remove_requested.emit(GameState.VESSEL_GAS_BOMBER)
if fleet.ships.size() != before - 1:
return _fail("the dev menu did not remove a vessel")
menu.unlimited_button.pressed.emit()
if not state.dev_unlimited_resources:
return _fail("the unlimited-resources toggle did not turn on")
for resource_type in state.resource_types():
state.bank_inventory.withdraw(resource_type, GameState.DEV_UNLIMITED_AMOUNT)
state.research_points = 0
state._process(0.0)
for resource_type in state.resource_types():
if state.banked_resource(resource_type) != GameState.DEV_UNLIMITED_AMOUNT:
return _fail("unlimited mode did not replenish %s" % resource_type)
if state.research_points != GameState.DEV_UNLIMITED_AMOUNT:
return _fail("unlimited mode did not replenish research points")
print("skill unlock and unlimited resources operate on persistent GameState")
var wave_before: int = state.wave_number
menu.wave_button.pressed.emit()
if menu.visible:
return _fail("starting a wave left the dev menu over the action")
if state.wave_number != wave_before + 1 or not state.wave_pending:
return _fail("the dev menu did not retain the immediate wave tool")
state.reset_new_game()
if state.dev_mode or state.dev_unlimited_resources:
return _fail("dev cheats leaked into a normal new game")
print("TEST_DEV_MENU: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_DEV_MENU: FAIL (%s)" % message)
quit(1)
return true

View file

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

View file

@ -73,6 +73,8 @@ func _process(delta: float) -> bool:
game_state.start_dev_game()
if not game_state.dev_mode:
return _fail("start_dev_game() did not set dev_mode")
if game_state.intro_active:
return _fail("dev mode still starts at the prologue instead of after it")
for resource_type in game_state.resource_types():
var banked: int = game_state.banked_resource(resource_type)
if banked != GameState.DEV_RESOURCE_AMOUNT:
@ -116,6 +118,8 @@ func _process(delta: float) -> bool:
game_state.reset_new_game()
if game_state.dev_mode:
return _fail("starting a normal game after a dev one left dev_mode set")
if game_state.dev_unlimited_resources:
return _fail("starting a normal game kept unlimited resources enabled")
if game_state.rocks != 0 or game_state.research_points != 0:
return _fail("starting a normal game after a dev one kept the dev stock")
print("starting a normal game afterwards clears dev mode entirely")

View file

@ -2,7 +2,7 @@ extends SceneTree
const MENUS: Array[String] = [
"BuildMenu", "CraftMenu", "LabMenu", "TransferMenu", "HangarMenu",
"FormationMenu", "SkillMenu", "PlanningMenu", "GarrisonMenu",
"FormationMenu", "SkillMenu", "PlanningMenu", "GarrisonMenu", "DevMenu",
]

View file

@ -434,7 +434,7 @@ func _process(delta: float) -> bool:
if elapsed < 0.3:
return false
# #140: the objective lives behind a button next to the map's,
# not on the HUD - it used to sit on top of the wave button.
# not on the HUD - it used to sit on top of the dev button.
if panel.visible:
return _fail("the objective panel is open before anyone pressed anything")
if not quest_button.visible:
@ -442,9 +442,9 @@ func _process(delta: float) -> bool:
var map_button: Control = scene.get_node("UI/MapButton")
if quest_button.get_rect().intersects(map_button.get_rect()):
return _fail("the quest button is drawn on top of the map button")
var wave_button: Control = scene.get_node("UI/DevWaveButton")
if quest_button.get_rect().intersects(wave_button.get_rect()):
return _fail("the quest button is drawn on top of the wave button")
var dev_button: Control = scene.get_node("UI/DevMenuButton")
if quest_button.get_rect().intersects(dev_button.get_rect()):
return _fail("the quest button is drawn on top of the dev button")
if absf(quest_button.get_rect().get_center().y - map_button.get_rect().get_center().y) > 1.0:
return _fail("the quest button does not sit beside the map button")
panel.open()
@ -458,7 +458,7 @@ func _process(delta: float) -> bool:
return _fail("the panel does not show the objective")
if panel.progress != Vector2i(0, 5):
return _fail("the panel reads %s, expected 0 of 5" % panel.progress)
print("quest button beside the map, clear of the wave button; panel: \"%s\" - %s (%d/%d)" \
print("quest button beside the map, clear of the dev button; panel: \"%s\" - %s (%d/%d)" \
% [panel.quest_label.text, panel.objective_label.text,
panel.progress.x, panel.progress.y])
panel.close()

View file

@ -224,20 +224,24 @@ func _process(delta: float) -> bool:
return _fail("killing the whole wave did not bring the quiet back")
print("the wave died and the quiet came back")
# --- G. the dev button ---------------------------------------
var dev_button: Control = scene.get_node("UI/DevWaveButton")
# --- G. the dev menu keeps the immediate wave tool -----------
var dev_button: Control = scene.get_node("UI/DevMenuButton")
var dev_menu: Control = scene.get_node("UI/DevMenu")
game_state.dev_mode = false
dev_button._process(0.0)
if dev_button.visible:
return _fail("the dev wave button is offered in a normal game")
return _fail("the dev menu button is offered in a normal game")
game_state.dev_mode = true
dev_button._process(0.0)
if not dev_button.visible:
return _fail("the dev wave button is hidden in dev mode")
var number_before: int = game_state.wave_number
return _fail("the dev menu button is hidden in dev mode")
dev_button.button.pressed.emit()
if not dev_menu.visible:
return _fail("the dev button did not open the dev menu")
var number_before: int = game_state.wave_number
dev_menu.wave_button.pressed.emit()
if game_state.wave_phase != GameState.WAVE_PHASE_ACTIVE:
return _fail("the button left the wave in \"%s\"" % game_state.wave_phase)
return _fail("the menu's wave tool left the wave in \"%s\"" % game_state.wave_phase)
if not game_state.wave_pending:
return _fail("the button turned the wave active without owing it to anyone")
if game_state.wave_number != number_before + 1:
@ -249,7 +253,7 @@ func _process(delta: float) -> bool:
spawner._process(0.0)
if _enemy_count() <= 0:
return _fail("the forced wave produced no enemies")
print("dev button: wave %d on the board at once, %d enemies" \
print("dev menu: wave %d on the board at once, %d enemies" \
% [game_state.wave_number, _enemy_count()])
game_state.dev_mode = false