From c325501999bb44e9f101a29c98f64635336f2728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Thu, 3 Sep 2026 20:05:33 +0200 Subject: [PATCH] Turn dev mode into sandbox menu --- core/GameState.gd | 56 ++++++++- tests/_dev_menu_preview.gd | 28 +++++ tests/_dev_menu_preview.gd.uid | 1 + tests/_dev_menu_preview.tscn | 6 + tests/test_dev_menu.gd | 95 +++++++++++++++ tests/test_dev_menu.gd.uid | 1 + tests/test_dev_mode.gd | 4 + tests/test_menu_scene_boundaries.gd | 2 +- tests/test_quest_chain.gd | 10 +- tests/test_wave_system.gd | 18 +-- ui/DevMenuButton.gd | 75 ++++++++++++ ui/DevMenuButton.gd.uid | 1 + ui/DevWaveButton.gd | 28 ----- ui/DevWaveButton.gd.uid | 1 - ui/IconButton.gd | 20 +++ ui/menus/DevMenu.gd | 149 +++++++++++++++++++++++ ui/menus/DevMenu.gd.uid | 1 + ui/menus/DevMenu.tscn | 182 ++++++++++++++++++++++++++++ world/Main.tscn | 27 +++-- 19 files changed, 651 insertions(+), 54 deletions(-) create mode 100644 tests/_dev_menu_preview.gd create mode 100644 tests/_dev_menu_preview.gd.uid create mode 100644 tests/_dev_menu_preview.tscn create mode 100644 tests/test_dev_menu.gd create mode 100644 tests/test_dev_menu.gd.uid create mode 100644 ui/DevMenuButton.gd create mode 100644 ui/DevMenuButton.gd.uid delete mode 100644 ui/DevWaveButton.gd delete mode 100644 ui/DevWaveButton.gd.uid create mode 100644 ui/menus/DevMenu.gd create mode 100644 ui/menus/DevMenu.gd.uid create mode 100644 ui/menus/DevMenu.tscn diff --git a/core/GameState.gd b/core/GameState.gd index 57949b1..f456287 100644 --- a/core/GameState.gd +++ b/core/GameState.gd @@ -185,9 +185,12 @@ var research_points: int = 0 # generation is static and dependency-free by design. This stays the single # place to set it - assigning dev_mode anywhere, tests included, reaches # world generation. +var dev_unlimited_resources: bool = false var dev_mode: bool = false: set(value): dev_mode = value + if not value: + dev_unlimited_resources = false WORLD_GEN.dev_planets_enabled = value # Explored-chunk history for the world map's fog-of-war reveal. Lives @@ -763,6 +766,7 @@ func reset_new_game() -> void: _distorder_drain_progress = 0.0 boost_depleted_notice = false research_points = 0 + dev_unlimited_resources = false dev_mode = false explored_chunks.clear() bank_inventory = INVENTORY.new() @@ -786,12 +790,15 @@ func reset_new_game() -> void: # the game immediately, without being so large that costs stop registering. const DEV_RESOURCE_AMOUNT: int = 2000 const DEV_RESEARCH_POINTS: int = 500 +const DEV_UNLIMITED_AMOUNT: int = 999999 -# A new game stocked for play-testing: every resource banked, research points -# to spend, and one of each planet type next to the spawn (WorldGen reads -# dev_mode for that). Offered by the main menu only when running from Godot - -# see MainMenu.gd - so it cannot be reached in a shipped build. +# A post-prologue game stocked for play-testing: every resource banked, +# research points to spend, and one of each planet type next to the spawn +# (WorldGen reads dev_mode for that). Offered by the main menu only when +# running from Godot - see MainMenu.gd - so it cannot be reached in a shipped +# build. Dev mode is for iterating on the game itself, not replaying the long +# scripted introduction before every check. # # Deliberately built ON TOP of reset_new_game() rather than beside it, so a # dev game is a normal game plus a stock rather than a second, separately @@ -799,12 +806,51 @@ const DEV_RESEARCH_POINTS: int = 500 func start_dev_game() -> void: reset_new_game() dev_mode = true + intro_active = false for resource_type in resource_types(): bank_inventory.set_amount(resource_type, DEV_RESOURCE_AMOUNT) research_points = DEV_RESEARCH_POINTS research_points_changed.emit(research_points) +# Development toggles deliberately live beside the state they affect. The +# menu is only a front-end; scene swaps cannot turn an infinite bank off or +# forget which doctrines were exposed. +func set_dev_unlimited_resources(enabled: bool) -> bool: + if enabled and not dev_mode: + return false + dev_unlimited_resources = enabled + if enabled: + _refill_dev_resources() + return true + + +func _refill_dev_resources() -> void: + for resource_type in resource_types(): + if bank_inventory.amount(resource_type) < DEV_UNLIMITED_AMOUNT: + bank_inventory.set_amount(resource_type, DEV_UNLIMITED_AMOUNT) + if research_points < DEV_UNLIMITED_AMOUNT: + research_points = DEV_UNLIMITED_AMOUNT + research_points_changed.emit(research_points) + + +func dev_unlock_all_skills() -> int: + if not dev_mode: + return 0 + var added: int = 0 + for node in SKILL_TREE: + var id: String = String(node.id) + if id in unlocked: + continue + unlocked.append(id) + added += 1 + # These permanent doctrines affect structures which may already exist. + # Sync once after the batch instead of once per node. + _sync_hub_transporters() + _sync_workshop_drones() + return added + + # --- building stock and production queue (#16) ----------------------------- # Since #12, a building cannot just deduct from the bank at the moment of @@ -2755,6 +2801,8 @@ func wave_cleared() -> void: func _process(delta: float) -> void: # Runs in the autoload, so mining and transport continue while the player # is on a planet's surface or nowhere near either. + if dev_mode and dev_unlimited_resources: + _refill_dev_resources() advance_waves(delta) _advance_production(delta) _advance_lab_research(delta) diff --git a/tests/_dev_menu_preview.gd b/tests/_dev_menu_preview.gd new file mode 100644 index 0000000..f031638 --- /dev/null +++ b/tests/_dev_menu_preview.gd @@ -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) diff --git a/tests/_dev_menu_preview.gd.uid b/tests/_dev_menu_preview.gd.uid new file mode 100644 index 0000000..9163642 --- /dev/null +++ b/tests/_dev_menu_preview.gd.uid @@ -0,0 +1 @@ +uid://bxsf8yo6lyhf8 diff --git a/tests/_dev_menu_preview.tscn b/tests/_dev_menu_preview.tscn new file mode 100644 index 0000000..a29c354 --- /dev/null +++ b/tests/_dev_menu_preview.tscn @@ -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") diff --git a/tests/test_dev_menu.gd b/tests/test_dev_menu.gd new file mode 100644 index 0000000..ad09482 --- /dev/null +++ b/tests/test_dev_menu.gd @@ -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 diff --git a/tests/test_dev_menu.gd.uid b/tests/test_dev_menu.gd.uid new file mode 100644 index 0000000..0a892cc --- /dev/null +++ b/tests/test_dev_menu.gd.uid @@ -0,0 +1 @@ +uid://dws3sq74nik4v diff --git a/tests/test_dev_mode.gd b/tests/test_dev_mode.gd index da7b4fa..7672874 100644 --- a/tests/test_dev_mode.gd +++ b/tests/test_dev_mode.gd @@ -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") diff --git a/tests/test_menu_scene_boundaries.gd b/tests/test_menu_scene_boundaries.gd index 6dfb496..70a02cd 100644 --- a/tests/test_menu_scene_boundaries.gd +++ b/tests/test_menu_scene_boundaries.gd @@ -2,7 +2,7 @@ extends SceneTree const MENUS: Array[String] = [ "BuildMenu", "CraftMenu", "LabMenu", "TransferMenu", "HangarMenu", - "FormationMenu", "SkillMenu", "PlanningMenu", "GarrisonMenu", + "FormationMenu", "SkillMenu", "PlanningMenu", "GarrisonMenu", "DevMenu", ] diff --git a/tests/test_quest_chain.gd b/tests/test_quest_chain.gd index 66f3807..893db02 100644 --- a/tests/test_quest_chain.gd +++ b/tests/test_quest_chain.gd @@ -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() diff --git a/tests/test_wave_system.gd b/tests/test_wave_system.gd index ab1e5e8..5cc3efa 100644 --- a/tests/test_wave_system.gd +++ b/tests/test_wave_system.gd @@ -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 diff --git a/ui/DevMenuButton.gd b/ui/DevMenuButton.gd new file mode 100644 index 0000000..ae77001 --- /dev/null +++ b/ui/DevMenuButton.gd @@ -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() diff --git a/ui/DevMenuButton.gd.uid b/ui/DevMenuButton.gd.uid new file mode 100644 index 0000000..0dd6891 --- /dev/null +++ b/ui/DevMenuButton.gd.uid @@ -0,0 +1 @@ +uid://c52odygv78ilg diff --git a/ui/DevWaveButton.gd b/ui/DevWaveButton.gd deleted file mode 100644 index bcf34cf..0000000 --- a/ui/DevWaveButton.gd +++ /dev/null @@ -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() diff --git a/ui/DevWaveButton.gd.uid b/ui/DevWaveButton.gd.uid deleted file mode 100644 index 1141957..0000000 --- a/ui/DevWaveButton.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://blhthsbawd7vg diff --git a/ui/IconButton.gd b/ui/IconButton.gd index d73a022..5e9597a 100644 --- a/ui/IconButton.gd +++ b/ui/IconButton.gd @@ -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: diff --git a/ui/menus/DevMenu.gd b/ui/menus/DevMenu.gd new file mode 100644 index 0000000..e96b43a --- /dev/null +++ b/ui/menus/DevMenu.gd @@ -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) diff --git a/ui/menus/DevMenu.gd.uid b/ui/menus/DevMenu.gd.uid new file mode 100644 index 0000000..e447598 --- /dev/null +++ b/ui/menus/DevMenu.gd.uid @@ -0,0 +1 @@ +uid://bvme310rk20ip diff --git a/ui/menus/DevMenu.tscn b/ui/menus/DevMenu.tscn new file mode 100644 index 0000000..e1e72c5 --- /dev/null +++ b/ui/menus/DevMenu.tscn @@ -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 diff --git a/world/Main.tscn b/world/Main.tscn index 849148d..67ceec6 100644 --- a/world/Main.tscn +++ b/world/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=44 format=3] +[gd_scene load_steps=45 format=3] [ext_resource type="PackedScene" path="res://ui/menus/BuildMenu.tscn" id="menu_build"] [ext_resource type="PackedScene" path="res://ui/menus/HangarMenu.tscn" id="menu_hangar"] [ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"] @@ -8,6 +8,7 @@ [ext_resource type="PackedScene" path="res://ui/menus/PlanningMenu.tscn" id="menu_planning"] [ext_resource type="PackedScene" path="res://ui/menus/GarrisonMenu.tscn" id="menu_garrison"] [ext_resource type="PackedScene" path="res://ui/menus/SkillMenu.tscn" id="menu_skill"] +[ext_resource type="PackedScene" path="res://ui/menus/DevMenu.tscn" id="menu_dev"] [ext_resource type="Script" path="res://world/Starfield.gd" id="1"] @@ -30,7 +31,7 @@ [ext_resource type="Script" path="res://vessels/GarrisonField.gd" id="34"] [ext_resource type="Script" path="res://ui/NarrationBanner.gd" id="36"] [ext_resource type="Script" path="res://world/IntroDirector.gd" id="37"] -[ext_resource type="Script" path="res://ui/DevWaveButton.gd" id="38"] +[ext_resource type="Script" path="res://ui/DevMenuButton.gd" id="38"] [ext_resource type="Script" path="res://logistics/RepairDroneField.gd" id="40"] [ext_resource type="Script" path="res://ui/BreachOverlay.gd" id="41"] [ext_resource type="Script" path="res://logistics/TransporterField.gd" id="23"] @@ -236,7 +237,7 @@ offset_bottom = 172 script = ExtResource("10") icon = 0 -[node name="DevWaveButton" type="Control" parent="UI"] +[node name="DevMenuButton" type="Control" parent="UI"] visible = false mouse_filter = 2 anchor_left = 1.0 @@ -244,15 +245,24 @@ anchor_right = 1.0 offset_left = -140.0 offset_right = -24.0 offset_top = 268.0 -offset_bottom = 384.0 +offset_bottom = 416.0 script = ExtResource("38") -[node name="Button" type="Control" parent="UI/DevWaveButton"] +[node name="Button" type="Control" parent="UI/DevMenuButton"] anchor_right = 1.0 -anchor_bottom = 1.0 +offset_bottom = 116.0 script = ExtResource("10") -icon = 5 -glow_color = Color(1, 0.45, 0.2, 1) +icon = 46 +glow_color = Color(0.55, 0.9, 1, 1) + +[node name="Label" type="Label" parent="UI/DevMenuButton"] +mouse_filter = 2 +anchor_right = 1.0 +offset_top = 118.0 +offset_bottom = 148.0 +theme_override_font_sizes/font_size = 18 +text = "DEV" +horizontal_alignment = 1 [node name="BreachOverlay" type="ColorRect" parent="UI"] visible = false @@ -498,6 +508,7 @@ icon = 1 [node name="PlanningMenu" parent="UI" instance=ExtResource("menu_planning")] [node name="GarrisonMenu" parent="UI" instance=ExtResource("menu_garrison")] [node name="SkillMenu" parent="UI" instance=ExtResource("menu_skill")] +[node name="DevMenu" parent="UI" instance=ExtResource("menu_dev")] [node name="QuestPanel" type="Control" parent="UI" groups=["modal_menus"]] visible = false mouse_filter = 2