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

75
ui/DevMenuButton.gd Normal file
View file

@ -0,0 +1,75 @@
extends Control
# Editor-only entry into the live sandbox. The modal scene owns presentation;
# this small controller owns the boundary to the world nodes it manipulates.
@onready var button: IconButton = $Button
@onready var menu: Control = get_node("../DevMenu")
@onready var fleet: Node = get_node("../../Fleet")
func _ready() -> void:
button.pressed.connect(_on_pressed)
menu.fleet_add_requested.connect(_on_fleet_add_requested)
menu.fleet_remove_requested.connect(_on_fleet_remove_requested)
menu.unlock_skills_requested.connect(_on_unlock_skills_requested)
menu.unlimited_resources_requested.connect(_on_unlimited_resources_requested)
menu.wave_requested.connect(_on_wave_requested)
visible = false
func _process(_delta: float) -> void:
var show_it: bool = GameState.dev_mode and not GameState.intro_active
if show_it != visible:
visible = show_it
if not show_it and menu.visible:
menu.close_menu()
func _on_pressed() -> void:
if not GameState.dev_mode:
return
menu.open_for(fleet)
func _on_fleet_add_requested(vessel_type: String) -> void:
if fleet.add_ship(vessel_type):
menu.set_status("Added %s" % GameState.vessel_name(vessel_type))
else:
menu.set_status("Fleet is full — unlock all skills for up to %d slots" \
% GameState.DEFAULT_FORMATION.size())
func _on_fleet_remove_requested(vessel_type: String) -> void:
var index: int = -1
for i in range(fleet.ships.size()):
if is_instance_valid(fleet.ships[i]) \
and String(fleet.ships[i].vessel_type) == vessel_type:
index = i
break
if index < 0:
menu.set_status("No %s is currently deployed" % GameState.vessel_name(vessel_type))
return
if fleet.store_ship(index) == "":
menu.set_status("Keep at least one vessel in the fleet")
return
menu.set_status("Removed %s" % GameState.vessel_name(vessel_type))
func _on_unlock_skills_requested() -> void:
var unlocked_count: int = GameState.dev_unlock_all_skills()
if unlocked_count > 0:
menu.set_status("Unlocked all skills (%d new)" % unlocked_count)
else:
menu.set_status("All skills are already unlocked")
func _on_unlimited_resources_requested(enabled: bool) -> void:
if not GameState.set_dev_unlimited_resources(enabled):
return
menu.set_status("Unlimited resources %s" % ("enabled" if enabled else "disabled"))
func _on_wave_requested() -> void:
GameState.force_wave_now()
menu.close_menu()

1
ui/DevMenuButton.gd.uid Normal file
View file

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

View file

@ -1,28 +0,0 @@
extends Control
# #110: a dev-only button that puts a wave on the board now.
#
# Waves are otherwise on a fifty-second clock, which makes anything that
# happens during one - the alert, the pathing, buildings taking damage, an
# outpost garrison actually earning its keep - slow to look at even once.
#
# Dev mode is the same gate the main menu already uses for the map editor:
# these are tools for building the game, and a player who installed a build
# has no use for them. The button does not exist outside it.
@onready var button: IconButton = get_node("Button")
func _ready() -> void:
button.pressed.connect(_on_pressed)
visible = false
func _process(_delta: float) -> void:
var show_it: bool = GameState.dev_mode and not GameState.intro_active
if show_it != visible:
visible = show_it
func _on_pressed() -> void:
GameState.force_wave_now()

View file

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

View file

@ -63,6 +63,9 @@ enum Icon {
QUEST,
# #179: a fleet-wide thrust burst, distinct from the Propulser vessel icon.
DASH,
# Development tools: three sliders, used only by the editor-only sandbox
# button. Appended to preserve every serialized icon number above.
DEV_TOOLS,
}
# Size the glyphs were originally drawn against. Every glyph dimension is
@ -323,6 +326,23 @@ func _draw() -> void:
_draw_quest_icon(center, pulse, s)
Icon.DASH:
_draw_dash_icon(center, pulse, s)
Icon.DEV_TOOLS:
_draw_dev_tools_icon(center, pulse, s)
func _draw_dev_tools_icon(center: Vector2, pulse: float, s: float) -> void:
# A compact bank of adjustment sliders. It reads as configuration rather
# than as the old BUILD glyph, which made the wave shortcut look like a
# second construction button.
for row in range(3):
var y: float = (-9.0 + float(row) * 9.0) * s
var left: Vector2 = center + Vector2(-13.0 * s, y)
var right: Vector2 = center + Vector2(13.0 * s, y)
NeonDraw.draw_glowing_line(self, left, right, _active_color(), pulse,
0.45 * s, core_color)
var knob_x: float = [-5.0, 7.0, -1.0][row] * s
NeonDraw.draw_glowing_circle(self, center + Vector2(knob_x, y),
2.7 * s, _active_color(), pulse, core_color)
func _draw_dash_icon(center: Vector2, pulse: float, s: float) -> void:

149
ui/menus/DevMenu.gd Normal file
View file

@ -0,0 +1,149 @@
extends "res://ui/menus/ModalMenu.gd"
signal fleet_add_requested(vessel_type: String)
signal fleet_remove_requested(vessel_type: String)
signal unlock_skills_requested
signal unlimited_resources_requested(enabled: bool)
signal wave_requested
const ICON_BUTTON = preload("res://ui/IconButton.gd")
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
const ROW_HEIGHT: float = 86.0
const DESIGN_SIZE := Vector2(720.0, 1280.0)
@onready var content: Control = $Content
@onready var fleet_summary: Label = $Content/FleetSummary
@onready var fleet_rows: VBoxContainer = $Content/FleetRows
@onready var unlock_button: IconButton = $Content/ToolButtons/UnlockSkills
@onready var unlimited_button: IconButton = $Content/ToolButtons/UnlimitedResources
@onready var wave_button: IconButton = $Content/ToolButtons/StartWave
@onready var unlimited_label: Label = $Content/ToolLabels/UnlimitedLabel
@onready var status: Label = $Content/StatusLabel
var fleet: Node = null
var row_controls: Dictionary = {}
func _ready() -> void:
super._ready()
$Content/CloseButton.pressed.connect(close_menu)
_build_fleet_rows()
unlock_button.pressed.connect(func(): unlock_skills_requested.emit())
unlimited_button.pressed.connect(func():
unlimited_resources_requested.emit(not GameState.dev_unlimited_resources))
wave_button.pressed.connect(func(): wave_requested.emit())
_layout_content()
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED and is_node_ready():
_layout_content()
func _layout_content() -> void:
# The project has a landscape reference viewport but is played in portrait
# too. A fixed design canvas keeps touch targets at their authored size and
# scales the complete composition uniformly into either shape.
var factor: float = minf(size.x / DESIGN_SIZE.x, size.y / DESIGN_SIZE.y)
content.scale = Vector2(factor, factor)
content.position = (size - DESIGN_SIZE * factor) * 0.5
func open_for(fleet_node: Node) -> void:
fleet = fleet_node
status.text = ""
open_menu()
refresh()
func set_status(message: String) -> void:
status.text = message
refresh()
func refresh() -> void:
if fleet == null or not is_instance_valid(fleet):
return
var total: int = fleet.ships.size()
fleet_summary.text = "CURRENT FLEET %d / %d" % [total, GameState.max_fleet_size()]
for definition in VESSEL_DEFINITIONS.fleet():
var vessel_type: String = String(definition.definition_id)
var count: int = 0
for ship in fleet.ships:
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
count += 1
var controls: Dictionary = row_controls.get(vessel_type, {})
if controls.is_empty():
continue
(controls.count as Label).text = str(count)
(controls.add as IconButton).enabled = total < GameState.max_fleet_size()
(controls.remove as IconButton).enabled = count > 0 and total > 1
unlock_button.highlighted = _all_skills_unlocked()
unlimited_button.highlighted = GameState.dev_unlimited_resources
unlimited_label.text = "UNLIMITED RESOURCES\n%s" \
% ("ON" if GameState.dev_unlimited_resources else "OFF")
func _all_skills_unlocked() -> bool:
for node in GameState.SKILL_TREE:
if not GameState.is_unlocked(String(node.id)):
return false
return true
func _build_fleet_rows() -> void:
for definition in VESSEL_DEFINITIONS.fleet():
var vessel_type: String = String(definition.definition_id)
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0.0, ROW_HEIGHT)
row.add_theme_constant_override("separation", 12)
fleet_rows.add_child(row)
var vessel_icon := ICON_BUTTON.new()
vessel_icon.custom_minimum_size = Vector2(76.0, 76.0)
vessel_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
vessel_icon.icon = int(definition.icon)
vessel_icon.glow_color = definition.glow_color
row.add_child(vessel_icon)
var name_label := Label.new()
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
name_label.text = String(definition.display_name)
name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
name_label.add_theme_font_size_override("font_size", 25)
row.add_child(name_label)
var remove_button := ICON_BUTTON.new()
remove_button.custom_minimum_size = Vector2(70.0, 70.0)
remove_button.icon = IconButton.Icon.MINUS
remove_button.glow_color = definition.glow_color
remove_button.pressed.connect(_request_remove.bind(vessel_type))
row.add_child(remove_button)
var count_label := Label.new()
count_label.custom_minimum_size = Vector2(48.0, 0.0)
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
count_label.add_theme_font_size_override("font_size", 31)
row.add_child(count_label)
var add_button := ICON_BUTTON.new()
add_button.custom_minimum_size = Vector2(70.0, 70.0)
add_button.icon = IconButton.Icon.PLUS
add_button.glow_color = definition.glow_color
add_button.pressed.connect(_request_add.bind(vessel_type))
row.add_child(add_button)
row_controls[vessel_type] = {
"count": count_label,
"add": add_button,
"remove": remove_button,
}
func _request_add(vessel_type: String) -> void:
fleet_add_requested.emit(vessel_type)
func _request_remove(vessel_type: String) -> void:
fleet_remove_requested.emit(vessel_type)

1
ui/menus/DevMenu.gd.uid Normal file
View file

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

182
ui/menus/DevMenu.tscn Normal file
View file

@ -0,0 +1,182 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="icon_button"]
[ext_resource type="Script" path="res://ui/menus/DevMenu.gd" id="menu_script"]
[node name="DevMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.96)
[node name="Content" type="Control" parent="."]
layout_mode = 0
offset_right = 720.0
offset_bottom = 1280.0
[node name="TitleLabel" type="Label" parent="Content"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 34.0
offset_right = -160.0
offset_bottom = 92.0
theme_override_font_sizes/font_size = 38
text = "DEV MENU"
horizontal_alignment = 1
vertical_alignment = 1
[node name="SubtitleLabel" type="Label" parent="Content"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 96.0
offset_right = -24.0
offset_bottom = 138.0
theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1)
theme_override_font_sizes/font_size = 20
text = "Changes apply to this play-test immediately"
horizontal_alignment = 1
[node name="CloseButton" type="Control" parent="Content"]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -140.0
offset_top = 20.0
offset_right = -24.0
offset_bottom = 136.0
script = ExtResource("icon_button")
icon = 1
glow_color = Color(0.55, 0.6, 1, 1)
[node name="RuleTop" type="ColorRect" parent="Content"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 32.0
offset_top = 151.0
offset_right = -32.0
offset_bottom = 153.0
color = Color(0.25, 0.85, 1, 0.55)
[node name="FleetSummary" type="Label" parent="Content"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 40.0
offset_top = 162.0
offset_right = -40.0
offset_bottom = 202.0
theme_override_font_sizes/font_size = 24
text = "CURRENT FLEET"
horizontal_alignment = 1
vertical_alignment = 1
[node name="FleetRows" type="VBoxContainer" parent="Content"]
anchor_right = 1.0
offset_left = 42.0
offset_top = 208.0
offset_right = -42.0
offset_bottom = 820.0
theme_override_constants/separation = 7
[node name="RuleBottom" type="ColorRect" parent="Content"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 32.0
offset_top = 836.0
offset_right = -32.0
offset_bottom = 838.0
color = Color(0.45, 0.9, 0.55, 0.55)
[node name="ToolsTitle" type="Label" parent="Content"]
mouse_filter = 2
anchor_right = 1.0
offset_top = 850.0
offset_bottom = 888.0
theme_override_font_sizes/font_size = 23
text = "SANDBOX TOOLS"
horizontal_alignment = 1
[node name="ToolButtons" type="HBoxContainer" parent="Content"]
anchor_right = 1.0
offset_left = 70.0
offset_top = 896.0
offset_right = -70.0
offset_bottom = 1012.0
theme_override_constants/separation = 56
alignment = 1
[node name="UnlockSkills" type="Control" parent="Content/ToolButtons"]
custom_minimum_size = Vector2(116, 116)
layout_mode = 2
script = ExtResource("icon_button")
icon = 18
glow_color = Color(0.45, 0.9, 0.55, 1)
[node name="UnlimitedResources" type="Control" parent="Content/ToolButtons"]
custom_minimum_size = Vector2(116, 116)
layout_mode = 2
script = ExtResource("icon_button")
icon = 29
glow_color = Color(1, 0.78, 0.25, 1)
[node name="StartWave" type="Control" parent="Content/ToolButtons"]
custom_minimum_size = Vector2(116, 116)
layout_mode = 2
script = ExtResource("icon_button")
icon = 24
glow_color = Color(1, 0.45, 0.2, 1)
[node name="ToolLabels" type="HBoxContainer" parent="Content"]
anchor_right = 1.0
offset_left = 28.0
offset_top = 1018.0
offset_right = -28.0
offset_bottom = 1098.0
theme_override_constants/separation = 8
alignment = 1
[node name="UnlockLabel" type="Label" parent="Content/ToolLabels"]
custom_minimum_size = Vector2(210, 76)
layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "UNLOCK ALL\nSKILLS"
horizontal_alignment = 1
vertical_alignment = 1
[node name="UnlimitedLabel" type="Label" parent="Content/ToolLabels"]
custom_minimum_size = Vector2(210, 76)
layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "UNLIMITED RESOURCES\nOFF"
horizontal_alignment = 1
vertical_alignment = 1
[node name="WaveLabel" type="Label" parent="Content/ToolLabels"]
custom_minimum_size = Vector2(210, 76)
layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "START ENEMY\nWAVE"
horizontal_alignment = 1
vertical_alignment = 1
[node name="StatusLabel" type="Label" parent="Content"]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 32.0
offset_top = -158.0
offset_right = -32.0
offset_bottom = -40.0
theme_override_colors/font_color = Color(0.55, 0.9, 1, 1)
theme_override_font_sizes/font_size = 23
text = ""
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2