#106/#107 (#98a/#98b): the Planning Center

Landed together: a board of standing orders nothing acts on is a notepad,
and a planner with nowhere to read the plan from has nothing to do.

The targets are GLOBAL, not per building. There is one plan for the whole
game, and a second planning centre is another place to read and edit it
rather than a competing one - per-building targets would let two centres
hold contradictory orders and tell the crafters to satisfy both.

The census is the harder half. Every vessel is counted exactly once wherever
it happens to be: flying in the fleet, parked in the hangar, stationed at an
outpost, or in the air between any two of those. Miss the in-flight ones and
the planner queues a replacement for a vessel that is merely en route. The
fleet is the one place GameState cannot see, so Fleet pushes its composition
on every change, alongside the cargo capacity and distorder count that
already went that way.

Three rules keep the planner from fighting the player. It tops up and never
rewrites, so an order placed by hand stays exactly where it is. Everything
counts, including what is already queued, or it re-queues the same shortfall
every tick and buries the player in hulls. And nothing happens without a
planning centre - though the plan is remembered when one is destroyed, so
rebuilding picks up where it left off.

Vessels on order are counted separately from the census on purpose: they do
not exist yet, so the readout can say "4 of 6, 2 building" rather than
claiming six. The per-row note says where the vessels actually are, because
a plan can look unmet while everything it asked for is sitting at an
outpost.

The build menu goes to three columns past eight entries. #54 already
tightened its rows to 223px to fit a fourth, explicitly to avoid a
scrollable menu, and a ninth structure needs a fifth row that will not fit
at any spacing a button, a label and a cost can share. Three columns take
nine into three rows - fewer than it uses today, not more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-23 14:21:52 +02:00
commit 0c595d775d
12 changed files with 883 additions and 16 deletions

View file

@ -1,12 +1,13 @@
class_name BuildingDefinitions class_name BuildingDefinitions
extends RefCounted extends RefCounted
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "outpost", "extractor", "transport_hub", "ship_lab", "gas_burner"] const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "outpost", "planning_center", "extractor", "transport_hub", "ship_lab", "gas_burner"]
const DEFINITIONS: Dictionary = { const DEFINITIONS: Dictionary = {
"mothership": preload("res://buildings/MothershipBuildingDefinition.tres"), "mothership": preload("res://buildings/MothershipBuildingDefinition.tres"),
"ship_crafter": preload("res://buildings/CrafterBuildingDefinition.tres"), "ship_crafter": preload("res://buildings/CrafterBuildingDefinition.tres"),
"command_center": preload("res://buildings/CommandBuildingDefinition.tres"), "command_center": preload("res://buildings/CommandBuildingDefinition.tres"),
"outpost": preload("res://buildings/OutpostBuildingDefinition.tres"), "outpost": preload("res://buildings/OutpostBuildingDefinition.tres"),
"planning_center": preload("res://buildings/PlanningBuildingDefinition.tres"),
"extractor": preload("res://buildings/ExtractorBuildingDefinition.tres"), "extractor": preload("res://buildings/ExtractorBuildingDefinition.tres"),
"transport_hub": preload("res://buildings/TransportHubBuildingDefinition.tres"), "transport_hub": preload("res://buildings/TransportHubBuildingDefinition.tres"),
"ship_lab": preload("res://buildings/ShipLabBuildingDefinition.tres"), "ship_lab": preload("res://buildings/ShipLabBuildingDefinition.tres"),

View file

@ -29,6 +29,18 @@ const GAS_BURNER_ENERGY_COLOR: Color = Color(0.85, 0.95, 0.15)
# #102. Olive rather than another blue or amber: every existing structure is # #102. Olive rather than another blue or amber: every existing structure is
# a place you go to DO something, and this is the one you leave people at. # a place you go to DO something, and this is the one you leave people at.
const OUTPOST_COLOR: Color = Color(0.72, 0.86, 0.45) const OUTPOST_COLOR: Color = Color(0.72, 0.86, 0.45)
# #106. A cool grey-blue: this is the one structure that does no work at all,
# it only decides what work the others do.
const PLANNING_COLOR: Color = Color(0.62, 0.74, 0.92)
# Planning centre: a standing board with rows of tallies on it, flanked by
# two slim pylons. Rectilinear on purpose - every other structure here is
# radial or trussed, and this one is an office.
const PLAN_BOARD_W: float = 86.0
const PLAN_BOARD_H: float = 62.0
const PLAN_ROW_COUNT: int = 4
const PLAN_PYLON_X: float = 104.0
const PLAN_PYLON_H: float = 54.0
# Outpost: a watch mast on a tripod footing, with two open berths beside it # Outpost: a watch mast on a tripod footing, with two open berths beside it
# where a garrison sits. Deliberately the least machinery of any structure # where a garrison sits. Deliberately the least machinery of any structure
@ -375,6 +387,41 @@ func _draw_command_center() -> void:
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9) NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9)
func _draw_planning_center() -> void:
var c: Color = PLANNING_COLOR
var lit: Color = Color(0.88, 0.94, 1.0)
# The board.
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
Vector2(-PLAN_BOARD_W, -PLAN_BOARD_H), Vector2(PLAN_BOARD_W, -PLAN_BOARD_H),
Vector2(PLAN_BOARD_W, PLAN_BOARD_H), Vector2(-PLAN_BOARD_W, PLAN_BOARD_H),
]), c, 1.0)
# Rows of tallies at differing lengths - a plan being met unevenly, which
# is what the building is for.
var fractions: Array = [0.86, 0.42, 0.68, 0.24]
for i in range(PLAN_ROW_COUNT):
var y: float = lerpf(-PLAN_BOARD_H + 16.0, PLAN_BOARD_H - 16.0,
float(i) / float(PLAN_ROW_COUNT - 1))
var left: float = -PLAN_BOARD_W + 14.0
var span: float = (PLAN_BOARD_W - 14.0) * 2.0
NeonDraw.draw_glowing_line(self, Vector2(left, y),
Vector2(left + span * float(fractions[i]), y), lit, 0.8, 0.34)
NeonDraw.draw_glowing_line(self, Vector2(left + span * float(fractions[i]), y),
Vector2(left + span, y), c, 0.35, 0.22)
# Pylons holding it up, and a footing bar under the whole thing.
for side in [-1.0, 1.0]:
var x: float = PLAN_PYLON_X * side
NeonDraw.draw_glowing_line(self, Vector2(x, -PLAN_PYLON_H),
Vector2(x, PLAN_PYLON_H), c, 0.75, 0.4)
NeonDraw.draw_glowing_line(self, Vector2(PLAN_BOARD_W * side, 0.0),
Vector2(x, 0.0), c, 0.55, 0.3)
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2(x, -PLAN_PYLON_H), 9.0), c, 0.9)
NeonDraw.draw_glowing_line(self, Vector2(-PLAN_PYLON_X, PLAN_PYLON_H),
Vector2(PLAN_PYLON_X, PLAN_PYLON_H), c, 0.5, 0.3)
func _draw_outpost() -> void: func _draw_outpost() -> void:
var c: Color = OUTPOST_COLOR var c: Color = OUTPOST_COLOR
var lit: Color = Color(0.94, 0.99, 0.82) var lit: Color = Color(0.94, 0.99, 0.82)

View file

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://buildings/BuildingDefinition.gd" id="1"]
[ext_resource type="PackedScene" path="res://buildings/PlanningBuildingVisual.tscn" id="2"]
[resource]
script = ExtResource("1")
definition_id = "planning_center"
display_name = "Planning Center"
description = "Holds standing orders for the whole game: say how many of each vessel should always exist, and the ship crafters are kept working until they do."
cost = 70
action_icon = 41
menu_icon = 41
visual_scene = ExtResource("2")
capabilities = PackedStringArray("planning")
map_color = Color(0.62, 0.74, 0.92, 1)
map_sides = 4
map_radius = 7.5
map_phase = 0.785398
menu_button_name = "PlanningButton"
menu_label_name = "PlanningLabel"
menu_cost_name = "PlanningCost"
unique = true
max_hp = 80

View file

@ -0,0 +1,13 @@
extends "res://buildings/BuildingVisualBase.gd"
# Drawn once, like the command centre and the outpost: a board of numbers
# does not animate, and per-frame redraws of art that never changes were a
# real framerate cost earlier on.
func _ready() -> void:
set_process(false)
queue_redraw()
func _draw() -> void:
register_draw()
_draw_planning_center()

View file

@ -0,0 +1,11 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://buildings/PlanningBuildingVisual.gd" id="1"]
[ext_resource type="Script" path="res://buildings/BuildingActionProvider.gd" id="2"]
[node name="Visual" type="Node2D"]
script = ExtResource("1")
[node name="ActionProvider" type="Node" parent="."]
script = ExtResource("2")
actions = PackedStringArray("planning", "transfer")

View file

@ -166,6 +166,14 @@ var lab_building_id: int = 0
@onready var garrison_close_button: IconButton = get_node("../UI/GarrisonMenu/CloseButton") @onready var garrison_close_button: IconButton = get_node("../UI/GarrisonMenu/CloseButton")
var garrison_building_id: int = 0 var garrison_building_id: int = 0
# #106: the standing-order board. No building id is remembered because the
# plan is global - there is one plan for the whole game, and a second
# planning centre is another place to read it rather than a competing one.
@onready var planning_menu: Control = get_node("../UI/PlanningMenu")
@onready var planning_panel: Control = get_node("../UI/PlanningMenu/Panel")
@onready var planning_status: Label = get_node("../UI/PlanningMenu/StatusLabel")
@onready var planning_close_button: IconButton = get_node("../UI/PlanningMenu/CloseButton")
@onready var skill_menu: Control = get_node("../UI/SkillMenu") @onready var skill_menu: Control = get_node("../UI/SkillMenu")
@onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree") @onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree")
@onready var skill_detail: Label = get_node("../UI/SkillMenu/DetailLabel") @onready var skill_detail: Label = get_node("../UI/SkillMenu/DetailLabel")
@ -263,6 +271,8 @@ func _ready() -> void:
build_close_button.pressed.connect(_close_build_menu) build_close_button.pressed.connect(_close_build_menu)
garrison_close_button.pressed.connect(_close_garrison_menu) garrison_close_button.pressed.connect(_close_garrison_menu)
garrison_panel.changed.connect(_refresh_garrison) garrison_panel.changed.connect(_refresh_garrison)
planning_close_button.pressed.connect(_close_planning_menu)
planning_panel.changed.connect(_refresh_planning)
_configure_build_menu_entries() _configure_build_menu_entries()
lab_close_button.pressed.connect(func(): lab_menu.visible = false) lab_close_button.pressed.connect(func(): lab_menu.visible = false)
lab_buy_button.pressed.connect(_on_lab_buy) lab_buy_button.pressed.connect(_on_lab_buy)
@ -598,6 +608,9 @@ func _on_action_pressed() -> void:
elif action == "formation": elif action == "formation":
formation_menu.visible = true formation_menu.visible = true
formation_editor.queue_redraw() formation_editor.queue_redraw()
elif action == "planning":
planning_menu.visible = true
_refresh_planning()
elif action == "garrison": elif action == "garrison":
garrison_building_id = current_building.building_id garrison_building_id = current_building.building_id
garrison_panel.building_id = garrison_building_id garrison_panel.building_id = garrison_building_id
@ -617,9 +630,17 @@ func _planet_under_fleet() -> Node2D:
return planet_field.get_planet_fleet_is_on() return planet_field.get_planet_fleet_is_on()
# Grid geometry, shared with the layout below. Rows of two, and the menu is # Grid geometry, shared with the layout below. The menu is rebuilt from
# rebuilt from whatever is currently researched (#21) rather than being a # whatever is currently researched (#21) rather than being a fixed
# fixed arrangement with holes punched in it. # arrangement with holes punched in it.
#
# Two profiles, because the grid ran out of room. #54 already tightened the
# rows to 223px to fit a fourth, explicitly to avoid a scrollable menu, and a
# ninth structure (#106's planning centre) needs a FIFTH row that will not
# fit at any spacing a button, a label and a cost can share. So past eight
# entries the grid goes to three columns instead, which takes nine into three
# rows - fewer than it uses today, not more. Narrower columns mean narrower
# labels, so those get a taller box to wrap into.
const MENU_ROW_TOPS: Array = [-300.0, -77.0, 146.0, 369.0] const MENU_ROW_TOPS: Array = [-300.0, -77.0, 146.0, 369.0]
const MENU_COL_X: float = 140.0 const MENU_COL_X: float = 140.0
const MENU_BUTTON_HALF: float = 64.0 const MENU_BUTTON_HALF: float = 64.0
@ -629,13 +650,17 @@ const MENU_LABEL_H: float = 52.0
const MENU_COST_HALF: float = 60.0 const MENU_COST_HALF: float = 60.0
const MENU_COST_H: float = 44.0 const MENU_COST_H: float = 44.0
# Gaps from each row's own top to where the label/cost start, sized to the # Gaps from each row's own top to where the label/cost start, sized to the
# shrunk MENU_*_H values above (#54: fitting a 4th row meant tightening # shrunk MENU_*_H values above.
# MENU_ROW_TOPS' spacing to 223px, less than the original 256px a row's
# button+label+cost needed - shrinking those closed the gap without
# resorting to a scrollable menu).
const MENU_LABEL_OFFSET: float = 106.0 const MENU_LABEL_OFFSET: float = 106.0
const MENU_COST_OFFSET: float = 162.0 const MENU_COST_OFFSET: float = 162.0
const MENU_WIDE_THRESHOLD: int = 8
const MENU_ROW_TOPS_WIDE: Array = [-330.0, -85.0, 160.0]
const MENU_COL_X_WIDE: float = 224.0
const MENU_LABEL_HALF_WIDE: float = 108.0
const MENU_LABEL_H_WIDE: float = 68.0
const MENU_COST_OFFSET_WIDE: float = 178.0
func _place_entry(node: Control, cx: float, top: float, half: float, height: float) -> void: func _place_entry(node: Control, cx: float, top: float, half: float, height: float) -> void:
node.offset_left = cx - half node.offset_left = cx - half
@ -659,16 +684,26 @@ func _layout_build_menu() -> void:
if visible: if visible:
shown.append(entry) shown.append(entry)
var wide: bool = shown.size() > MENU_WIDE_THRESHOLD
var columns: int = 3 if wide else 2
var tops: Array = MENU_ROW_TOPS_WIDE if wide else MENU_ROW_TOPS
var col_x: float = MENU_COL_X_WIDE if wide else MENU_COL_X
var label_half: float = MENU_LABEL_HALF_WIDE if wide else MENU_LABEL_HALF
var label_h: float = MENU_LABEL_H_WIDE if wide else MENU_LABEL_H
var cost_offset: float = MENU_COST_OFFSET_WIDE if wide else MENU_COST_OFFSET
for i in range(shown.size()): for i in range(shown.size()):
var row: int = i / 2 var row: int = i / columns
# Centred when it is the only one on its row, rather than hanging in # A short last row is CENTRED rather than left-packed, so it does not
# the left column looking like something is missing beside it. # read as a row with something missing beside it.
var alone: bool = (i == shown.size() - 1) and (i % 2 == 0) var in_row: int = mini(shown.size() - row * columns, columns)
var cx: float = 0.0 if alone else (-MENU_COL_X if i % 2 == 0 else MENU_COL_X) var col: int = i % columns
var top: float = MENU_ROW_TOPS[mini(row, MENU_ROW_TOPS.size() - 1)] var cx: float = (float(col) - (float(in_row) - 1.0) * 0.5) * col_x * 2.0 \
/ maxf(float(columns) - 1.0, 1.0)
var top: float = tops[mini(row, tops.size() - 1)]
_place_entry(shown[i][1], cx, top, MENU_BUTTON_HALF, MENU_BUTTON_H) _place_entry(shown[i][1], cx, top, MENU_BUTTON_HALF, MENU_BUTTON_H)
_place_entry(shown[i][2], cx, top + MENU_LABEL_OFFSET, MENU_LABEL_HALF, MENU_LABEL_H) _place_entry(shown[i][2], cx, top + MENU_LABEL_OFFSET, label_half, label_h)
_place_entry(shown[i][3], cx, top + MENU_COST_OFFSET, MENU_COST_HALF, MENU_COST_H) _place_entry(shown[i][3], cx, top + cost_offset, MENU_COST_HALF, MENU_COST_H)
func _refresh_build_status() -> void: func _refresh_build_status() -> void:
@ -731,6 +766,25 @@ func _refresh_garrison() -> void:
% [crewed, ordered] % [crewed, ordered]
# --- the planning centre's standing orders (#106) ---------------------------
func _close_planning_menu() -> void:
planning_menu.visible = false
func _refresh_planning() -> void:
planning_panel.refresh()
var short: PackedStringArray = PackedStringArray()
for vessel_type in GameState.planned_vessel_types():
var missing: int = GameState.vessel_shortfall(String(vessel_type))
if missing > 0:
short.append("%d %s" % [missing, GameState.vessel_name(String(vessel_type))])
if short.is_empty():
planning_status.text = "Every standing order is met or already being built"
else:
planning_status.text = "Short: %s" % ", ".join(short)
func cost_of(type: String) -> int: func cost_of(type: String) -> int:
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
return int(definition.cost) if definition != null else 0 return int(definition.cost) if definition != null else 0

View file

@ -20,6 +20,10 @@ const WORLD_GEN = preload("res://world/WorldGen.gd")
var has_return_position: bool = false var has_return_position: bool = false
var surviving_ship_count: int = STARTING_SHIP_COUNT var surviving_ship_count: int = STARTING_SHIP_COUNT
var surviving_vessel_types: PackedStringArray = PackedStringArray() var surviving_vessel_types: PackedStringArray = PackedStringArray()
# #106: a mirror of what is flying, pushed by Fleet._sync_composition_state()
# whenever the composition changes. GameState cannot see scene nodes, and the
# census the Planning Center works from has to include the fleet.
var fleet_vessel_types: PackedStringArray = PackedStringArray()
var return_world_position: Vector2 = Vector2.ZERO var return_world_position: Vector2 = Vector2.ZERO
var return_camera_zoom: float = 1.0 var return_camera_zoom: float = 1.0
@ -309,6 +313,8 @@ const BUILDING_COMMAND_CENTER: String = "command_center"
# #102: a forward position the fleet can garrison, researched early because # #102: a forward position the fleet can garrison, researched early because
# it is the answer to waves going after what you have built. # it is the answer to waves going after what you have built.
const BUILDING_OUTPOST: String = "outpost" const BUILDING_OUTPOST: String = "outpost"
# #106: where standing orders for the whole fleet are kept.
const BUILDING_PLANNING_CENTER: String = "planning_center"
# Extractors are not placed in space like the others - they are built ON a # Extractors are not placed in space like the others - they are built ON a
# planet's surface and tracked separately (see `extractors`). # planet's surface and tracked separately (see `extractors`).
const BUILDING_EXTRACTOR: String = "extractor" const BUILDING_EXTRACTOR: String = "extractor"
@ -382,6 +388,16 @@ const SKILL_TREE: Array = [
"x": 0.66, "outpost_bonus": 1, "research_points": 2, "x": 0.66, "outpost_bonus": 1, "research_points": 2,
"desc": "Permanent: +1 berth at every outpost, including ones already built.", "desc": "Permanent: +1 berth at every outpost, including ones already built.",
}, },
{
# Deep on purpose, unlike forward_bases: standing orders are a thing
# you want once the fleet is large enough that keeping track of it by
# hand has become work, and it has shipyards behind it because it
# manages what shipyards do.
"id": "logistics_command", "name": "Logistics Command", "cost": 140,
"requires": "vessel_science", "x": 0.94,
"unlocks": BUILDING_PLANNING_CENTER, "research_points": 3,
"desc": "Build a planning center: set how many of each vessel should always exist, and the crafters are kept working until they do.",
},
{ {
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics", "id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1, "x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1,
@ -556,6 +572,7 @@ func reset_new_game() -> void:
has_return_position = false has_return_position = false
surviving_ship_count = STARTING_SHIP_COUNT surviving_ship_count = STARTING_SHIP_COUNT
surviving_vessel_types = PackedStringArray() surviving_vessel_types = PackedStringArray()
fleet_vessel_types = PackedStringArray()
return_world_position = Vector2.ZERO return_world_position = Vector2.ZERO
return_camera_zoom = 1.0 return_camera_zoom = 1.0
planet_name = "" planet_name = ""
@ -576,6 +593,7 @@ func reset_new_game() -> void:
extractors.clear() extractors.clear()
transporters.clear() transporters.clear()
planet_capture.clear() planet_capture.clear()
vessel_targets.clear()
reset_waves() reset_waves()
reset_progression() reset_progression()
reset_formation() reset_formation()
@ -1397,6 +1415,179 @@ func _return_surplus_garrison(b: Dictionary) -> void:
}) })
# --- standing orders, and the census behind them (#106) --------------------
#
# "how many ships of a give type should always be available over the whole
# game (fleet + storage + outposts)".
#
# The targets are GLOBAL, not per building. There is one plan for the whole
# game; a second Planning Center is a second place to read and edit the same
# plan, not a competing one. Storing them per building would mean two
# centres could hold contradictory orders and the crafters would be told to
# satisfy both.
#
# The census is the harder half, and the rule is that every vessel is
# counted exactly once, wherever it happens to be at that instant - flying
# in the fleet, parked in the hangar, stationed at an outpost, or in the air
# between any two of those. Miss the in-flight ones and the planner queues a
# replacement for a vessel that is merely en route.
var vessel_targets: Dictionary = {}
func has_planning_center() -> bool:
for b in buildings:
if building_has_capability(b, "planning"):
return true
return false
func vessel_target(vessel_type: String) -> int:
return int(vessel_targets.get(vessel_type, 0))
func set_vessel_target(vessel_type: String, count: int) -> int:
if not vessel_is_fleet_eligible(vessel_type):
return 0
var final_count: int = maxi(count, 0)
if final_count == 0:
vessel_targets.erase(vessel_type)
else:
vessel_targets[vessel_type] = final_count
return final_count
func adjust_vessel_target(vessel_type: String, delta: int) -> int:
return set_vessel_target(vessel_type, vessel_target(vessel_type) + delta)
func planned_vessel_types() -> PackedStringArray:
var out: PackedStringArray = PackedStringArray()
for definition in VESSEL_DEFINITIONS.all():
if bool(definition.fleet_eligible):
out.append(String(definition.definition_id))
return out
# How many of this type exist anywhere. Broken out per place rather than
# summed inline so the census can be read - and tested - one location at a
# time.
func vessel_census(vessel_type: String) -> int:
return vessel_count_in_fleet(vessel_type) \
+ vessel_count_in_hangar(vessel_type) \
+ vessel_count_in_garrisons(vessel_type) \
+ vessel_count_in_transit(vessel_type)
func vessel_count_in_fleet(vessel_type: String) -> int:
var n: int = 0
for flying in fleet_vessel_types:
if String(flying) == vessel_type:
n += 1
return n
func vessel_count_in_hangar(vessel_type: String) -> int:
var n: int = 0
for stored in stored_vessels:
if String(stored) == vessel_type:
n += 1
return n
func vessel_count_in_garrisons(vessel_type: String) -> int:
var n: int = 0
for id in outpost_ids():
n += garrison_crew_of(id, vessel_type)
return n
# Both directions of flight: a finished hull on its way to the mothership,
# and a vessel being moved to or from an outpost. All of them are vessels
# that exist and will land somewhere.
func vessel_count_in_transit(vessel_type: String) -> int:
var n: int = 0
for delivery in vessel_deliveries:
if String(delivery.type) == vessel_type:
n += 1
return n
# Ordered but not yet built. Kept separate from the census on purpose: these
# do not exist yet, so a readout has to be able to say "4 of 6, 2 building"
# rather than claiming six.
func vessel_count_on_order(vessel_type: String) -> int:
var n: int = 0
for b in buildings:
if not can_produce(String(b.type)):
continue
for order in b.get("orders", []):
if String(order.product) == vessel_type:
n += 1
return n
func vessel_shortfall(vessel_type: String) -> int:
return maxi(vessel_target(vessel_type)
- vessel_census(vessel_type) - vessel_count_on_order(vessel_type), 0)
# --- the planner working the crafters (#107) -------------------------------
#
# "the planning center automatically manage the ship craft building queues to
# ensure that the needed vessels are produced to fullfill the constraints."
#
# Three rules keep this from fighting the player:
#
# It TOPS UP, never rewrites. Orders placed by hand are left exactly where
# they are; the planner only ever appends, and only for a type that is
# genuinely short. Cancelling one of the player's orders to make room for
# one of its own would be the game arguing with them.
#
# Everything counts. A vessel already queued, already flying to the
# mothership, or already stationed at an outpost is a vessel that exists or
# shortly will - see vessel_shortfall(). Without that it re-queues the same
# shortfall every tick and buries the player in hulls.
#
# Nothing happens without a planning centre. Destroy it and the standing
# orders stop being acted on, though they are remembered, so rebuilding
# picks up where it left off rather than starting from a blank plan.
func _crafter_for_planning() -> Dictionary:
# The shortest queue, so orders spread across the crafters that exist
# rather than piling onto whichever happens to be first in the list.
var best: Dictionary = {}
var best_queue: int = 0
for b in buildings:
if not can_produce(String(b.type)):
continue
var queued: int = (b.get("orders", []) as Array).size()
if best.is_empty() or queued < best_queue:
best = b
best_queue = queued
return best
func _advance_planning() -> void:
if vessel_targets.is_empty() or not has_planning_center():
return
for vessel_type in vessel_targets:
var type_id: String = String(vessel_type)
if not vessel_is_unlocked(type_id):
continue
var missing: int = vessel_shortfall(type_id)
if missing <= 0:
continue
var crafter: Dictionary = _crafter_for_planning()
if crafter.is_empty():
return
# ONE per pass, not the whole shortfall. The queue is spread across
# crafters by _crafter_for_planning(), and asking for the whole
# shortfall at once would put it all on the shortest queue before
# that had a chance to change.
queue_order(int(crafter.id), type_id)
func queue_lab_research(lab_id: int, upgrade_id: String) -> bool: func queue_lab_research(lab_id: int, upgrade_id: String) -> bool:
var lab: Dictionary = find_building(lab_id) var lab: Dictionary = find_building(lab_id)
if not building_has_capability(lab, "vessel_research") or upgrade_id == "": if not building_has_capability(lab, "vessel_research") or upgrade_id == "":
@ -1991,6 +2182,7 @@ func _process(delta: float) -> void:
_advance_lab_research(delta) _advance_lab_research(delta)
_advance_vessel_deliveries(delta) _advance_vessel_deliveries(delta)
_advance_garrison_dispatch() _advance_garrison_dispatch()
_advance_planning()
_advance_resource_converters(delta) _advance_resource_converters(delta)
_advance_distorder_drain(delta) _advance_distorder_drain(delta)
for e in extractors: for e in extractors:

View file

@ -0,0 +1,246 @@
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_planning_center.gd
# Forgejo #106 and #107 (#98a, #98b): the Planning Center.
#
# "let the player tell the system how many ships of a give type should
# always be available over the whole game (fleet + storage + outposts) ...
# the planning center automatically manage the ship craft building queues
# to ensure that the needed vessels are produced to fullfill the
# constraints."
#
# Covers:
# A. the census counts a vessel exactly once wherever it is - flying,
# stored, stationed at an outpost, or in the air between any two
# B. a shortfall is queued at a crafter, and queued vessels count, so it
# does not re-order the same shortfall every tick
# C. it tops up rather than rewriting: an order placed by hand survives,
# and the planner appends beside it
# D. it stops when the target is met, and starts again when one is lost
# E. nothing happens without a planning centre, but the plan is remembered
# across losing and rebuilding one
# F. the panel drives the real targets through the real buttons
var game_state: Node
var scene: Node
var controller: Node
var menu: Control
var panel: Control
var crafter_id: int = 0
var outpost_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
# GameState is an autoload: a bare GameState.<instance member> does not
# compile in a SceneTree script, so the node is fetched by name instead.
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.stored_vessels.clear()
game_state.vessel_deliveries.clear()
game_state.vessel_targets.clear()
game_state.fleet_vessel_types = PackedStringArray()
game_state.reset_progression()
game_state.reset_waves()
game_state.unlocked.append_array(["shipyards", "coordination", "forward_bases",
"vessel_science", "logistics_command"])
func _fail(message: String) -> bool:
print("TEST_PLANNING_CENTER: FAIL (%s)" % message)
quit(1)
return true
func _queued_at_crafter() -> int:
return game_state.pending_orders_of(crafter_id, GameState.VESSEL_INTERCEPTOR)
func _state_cases() -> bool:
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
crafter_id = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0))
outpost_id = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(-900.0, 0.0))
var interceptor: String = GameState.VESSEL_INTERCEPTOR
# --- A. one vessel, one count, wherever it is ------------------------
if game_state.vessel_census(interceptor) != 0:
return _fail("an empty game counts %d vessels" % game_state.vessel_census(interceptor))
game_state.fleet_vessel_types = PackedStringArray([interceptor, interceptor])
game_state.stored_vessels.append(interceptor)
game_state.set_garrison_desired(outpost_id, interceptor, 1)
game_state.garrison_crew(outpost_id).append(interceptor)
game_state.vessel_deliveries.append({"type": interceptor, "from": Vector2.ZERO,
"to": Vector2(400.0, 0.0), "progress": 0.0, "to_outpost": 0})
if game_state.vessel_count_in_fleet(interceptor) != 2:
return _fail("the fleet reads %d" % game_state.vessel_count_in_fleet(interceptor))
if game_state.vessel_count_in_hangar(interceptor) != 1:
return _fail("the hangar reads %d" % game_state.vessel_count_in_hangar(interceptor))
if game_state.vessel_count_in_garrisons(interceptor) != 1:
return _fail("the garrisons read %d" % game_state.vessel_count_in_garrisons(interceptor))
if game_state.vessel_count_in_transit(interceptor) != 1:
return _fail("in transit reads %d" % game_state.vessel_count_in_transit(interceptor))
if game_state.vessel_census(interceptor) != 5:
return _fail("2 flying + 1 stored + 1 stationed + 1 moving counted as %d" \
% game_state.vessel_census(interceptor))
print("census: 2 flying, 1 stored, 1 stationed, 1 moving = %d" \
% game_state.vessel_census(interceptor))
# --- E (first half). no centre, no action ---------------------------
game_state.set_vessel_target(interceptor, 8)
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != 0:
return _fail("orders were placed with no planning centre built")
print("with no planning centre, a standing order does nothing")
game_state.add_building(GameState.BUILDING_PLANNING_CENTER, Vector2(900.0, 0.0))
if not game_state.has_planning_center():
return _fail("the planning centre was built but is not recognised")
# --- B. the shortfall is queued, once ------------------------------
if game_state.vessel_shortfall(interceptor) != 3:
return _fail("a target of 8 against 5 reads a shortfall of %d" \
% game_state.vessel_shortfall(interceptor))
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != 3:
return _fail("20 passes at a shortfall of 3 queued %d orders" % _queued_at_crafter())
if game_state.vessel_shortfall(interceptor) != 0:
return _fail("the shortfall is still %d with 3 on the slipway" \
% game_state.vessel_shortfall(interceptor))
print("a shortfall of 3 queued exactly 3, and 20 more passes added none")
# --- C. hand-placed orders are left alone ---------------------------
game_state.queue_order(crafter_id, GameState.VESSEL_SHIELD)
var by_hand: int = game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD)
game_state.set_vessel_target(interceptor, 10)
for i in range(20):
game_state._advance_planning()
if game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD) != by_hand:
return _fail("the planner cancelled a shield the player ordered by hand")
if _queued_at_crafter() != 5:
return _fail("raising the target to 10 left %d interceptors queued" % _queued_at_crafter())
print("a hand-placed shield survives; the planner appended beside it")
# --- D. it stops, and it reacts to a loss ---------------------------
game_state.set_vessel_target(interceptor, 5)
var queued_before: int = _queued_at_crafter()
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != queued_before:
return _fail("lowering the target changed the queue by %d" \
% (_queued_at_crafter() - queued_before))
# Losing one from the fleet is a real shortfall again.
game_state.set_vessel_target(interceptor, 12)
var before_loss: int = _queued_at_crafter()
for i in range(20):
game_state._advance_planning()
var after_top_up: int = _queued_at_crafter()
game_state.fleet_vessel_types = PackedStringArray([interceptor])
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() <= after_top_up:
return _fail("losing a vessel did not put a replacement on the slipway")
print("target met -> nothing queued; a vessel lost -> %d queued (was %d)" \
% [_queued_at_crafter() - after_top_up, before_loss])
# --- E (second half). the plan outlives the building ----------------
for b in game_state.buildings:
if game_state.building_has_capability(b, "planning"):
game_state.destroy_building(int(b.id))
break
if game_state.has_planning_center():
return _fail("the planning centre survived being destroyed")
if game_state.vessel_target(interceptor) != 12:
return _fail("losing the building erased the plan")
var frozen: int = _queued_at_crafter()
game_state.fleet_vessel_types = PackedStringArray()
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != frozen:
return _fail("the planner kept working after its building was destroyed")
print("destroyed: the plan is remembered, and nothing acts on it")
return false
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if _state_cases():
return true
game_state.buildings.clear()
game_state.vessel_targets.clear()
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
controller = scene.get_node("BuildController")
menu = scene.get_node("UI/PlanningMenu")
panel = scene.get_node("UI/PlanningMenu/Panel")
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
scene.get_node("BuildingField").place(GameState.BUILDING_PLANNING_CENTER,
Vector2(900.0, 0.0))
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.2:
return false
# --- F. the real panel, through the real buttons -------------
var node: Node2D = null
for b in scene.get_node("BuildingField").spawned:
if is_instance_valid(b) and b.building_type == GameState.BUILDING_PLANNING_CENTER:
node = b
if node == null:
return _fail("no planning centre node was placed")
if node.primary_action() != "planning":
return _fail("its primary action is \"%s\"" % node.primary_action())
controller.current_building = node
controller._on_action_pressed()
if not menu.visible:
return _fail("the planning centre did not open its menu")
var row: Dictionary = {}
for candidate in panel._rows:
if String(candidate.type) == GameState.VESSEL_INTERCEPTOR:
row = candidate
if row.is_empty():
return _fail("the panel has no row for the interceptor")
row.plus.pressed.emit()
row.plus.pressed.emit()
row.plus.pressed.emit()
if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 3:
return _fail("three presses of + set a target of %d" \
% game_state.vessel_target(GameState.VESSEL_INTERCEPTOR))
row.minus.pressed.emit()
if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 2:
return _fail("- did not lower the target")
if not row.count.text.ends_with("/2"):
return _fail("the row reads \"%s\" - it should show have over want" % row.count.text)
# The note is what makes an unmet plan legible: it says where the
# vessels actually are.
if row.note.text.strip_edges() == "" and game_state.vessel_census(
GameState.VESSEL_INTERCEPTOR) > 0:
return _fail("vessels exist but the row says nothing about where they are")
print("the panel drives the real plan: + + + - leaves \"%s\" (%s)" \
% [row.count.text, row.note.text])
var status: String = scene.get_node("UI/PlanningMenu/StatusLabel").text
if status.strip_edges() == "":
return _fail("the planning menu reports nothing at all")
print("status: \"%s\"" % status)
print("TEST_PLANNING_CENTER: PASS")
quit(0)
return true
return false

View file

@ -53,6 +53,8 @@ enum Icon {
# #102: appended, like everything above - inserting anywhere earlier # #102: appended, like everything above - inserting anywhere earlier
# renumbers every icon int already stored in every scene. # renumbers every icon int already stored in every scene.
OUTPOST, OUTPOST,
# #106: appended, same rule.
PLANNING_CENTER,
} }
# Size the glyphs were originally drawn against. Every glyph dimension is # Size the glyphs were originally drawn against. Every glyph dimension is
@ -296,6 +298,8 @@ func _draw() -> void:
_draw_distorder_vessel(center, pulse, s) _draw_distorder_vessel(center, pulse, s)
Icon.OUTPOST: Icon.OUTPOST:
_draw_outpost_icon(center, pulse, s) _draw_outpost_icon(center, pulse, s)
Icon.PLANNING_CENTER:
_draw_planning_icon(center, pulse, s)
func _draw_close(center: Vector2, pulse: float, s: float) -> void: func _draw_close(center: Vector2, pulse: float, s: float) -> void:
@ -664,6 +668,26 @@ func _draw_ship_lab_icon(center: Vector2, pulse: float, s: float) -> void:
_active_color(), pulse, 0.35 * s, core_color) _active_color(), pulse, 0.35 * s, core_color)
func _draw_planning_icon(center: Vector2, pulse: float, s: float) -> void:
# A tally board: a frame with three bars of different lengths inside it.
# Nothing else in the set is rectilinear and horizontal, and the meaning
# wanted is "how many of each", not "a place that builds things" - the
# crafter's cradle already owns that.
var w: float = 12.0 * s
var h: float = 11.0 * s
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
center + Vector2(-w, -h), center + Vector2(w, -h),
center + Vector2(w, h), center + Vector2(-w, h),
]), _active_color(), pulse, core_color)
var lengths: Array = [0.82, 0.5, 0.66]
for i in range(lengths.size()):
var y: float = center.y + (float(i) - 1.0) * 6.0 * s
var left: float = center.x - w + 3.0 * s
NeonDraw.draw_glowing_line(self, Vector2(left, y),
Vector2(left + (2.0 * w - 6.0 * s) * float(lengths[i]), y),
_active_color(), pulse, 0.4 * s, core_color)
func _draw_outpost_icon(center: Vector2, pulse: float, s: float) -> void: func _draw_outpost_icon(center: Vector2, pulse: float, s: float) -> void:
# A watchtower with two vessels holding station off it. The hub's # A watchtower with two vessels holding station off it. The hub's
# octagon already means "things are stationed here and go out from # octagon already means "things are stationed here and go out from

150
ui/PlanningPanel.gd Normal file
View file

@ -0,0 +1,150 @@
extends Control
# #106 (#98a): where the player says how many of each vessel should always
# exist.
#
# Like GarrisonPanel it builds its own rows from the catalog, so a new
# fleet-eligible vessel gets a row the day it is added without touching the
# scene. Unlike GarrisonPanel the targets are GLOBAL - there is one plan for
# the whole game, and a second planning centre is another place to read the
# same plan rather than a competing one.
#
# Each row reads "have / want" with what is being built alongside it, so a
# plan that is merely waiting on a production queue is distinguishable from
# one nothing is being done about. That distinction is the whole readout:
# without it a player looking at "2 / 5" cannot tell whether the system is
# working.
signal changed
const ROW_HEIGHT: float = 84.0
const ROW_GAP: float = 8.0
const BUTTON_SIZE: float = 68.0
const NAME_FONT_SIZE: int = 28
const COUNT_FONT_SIZE: int = 30
const NOTE_FONT_SIZE: int = 20
const HEADER_FONT_SIZE: int = 22
var _rows: Array = []
var _header: Label
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
_header = _make_label("", HEADER_FONT_SIZE, Color(0.62, 0.78, 0.86))
_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
for vessel_type in GameState.planned_vessel_types():
_rows.append(_make_row(String(vessel_type)))
resized.connect(_layout)
_layout()
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
var label := Label.new()
label.text = text_value
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", colour)
label.add_theme_color_override("font_outline_color", Color(0.01, 0.01, 0.035, 0.95))
label.add_theme_constant_override("outline_size", 2)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
add_child(label)
return label
func _make_row(vessel_type: String) -> Dictionary:
var definition: Resource = GameState.vessel_definition(vessel_type)
var colour: Color = definition.glow_color if definition != null else Color.WHITE
var name_label: Label = _make_label(GameState.vessel_name(vessel_type),
NAME_FONT_SIZE, colour)
var note_label: Label = _make_label("", NOTE_FONT_SIZE, Color(0.62, 0.78, 0.86))
var count_label: Label = _make_label("0/0", COUNT_FONT_SIZE, Color(0.94, 0.98, 1.0))
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
var minus := IconButton.new()
minus.icon = IconButton.Icon.MINUS
minus.glow_color = colour
minus.pressed.connect(_on_adjust.bind(vessel_type, -1))
add_child(minus)
var plus := IconButton.new()
plus.icon = IconButton.Icon.PLUS
plus.glow_color = colour
plus.pressed.connect(_on_adjust.bind(vessel_type, 1))
add_child(plus)
return {"type": vessel_type, "name": name_label, "note": note_label,
"count": count_label, "minus": minus, "plus": plus}
func _on_adjust(vessel_type: String, delta: int) -> void:
GameState.adjust_vessel_target(vessel_type, delta)
refresh()
changed.emit()
func _layout() -> void:
if _rows.is_empty():
return
_header.position = Vector2.ZERO
_header.size = Vector2(size.x, 40.0)
var y: float = 46.0
for row in _rows:
row.name.position = Vector2(0.0, y)
row.name.size = Vector2(size.x * 0.44, ROW_HEIGHT * 0.58)
row.note.position = Vector2(0.0, y + ROW_HEIGHT * 0.54)
row.note.size = Vector2(size.x * 0.52, ROW_HEIGHT * 0.42)
var right: float = size.x
row.plus.position = Vector2(right - BUTTON_SIZE, y + (ROW_HEIGHT - BUTTON_SIZE) * 0.5)
row.plus.size = Vector2(BUTTON_SIZE, BUTTON_SIZE)
row.count.position = Vector2(right - BUTTON_SIZE * 3.2, y)
row.count.size = Vector2(BUTTON_SIZE * 2.2, ROW_HEIGHT)
row.minus.position = Vector2(right - BUTTON_SIZE * 4.2,
y + (ROW_HEIGHT - BUTTON_SIZE) * 0.5)
row.minus.size = Vector2(BUTTON_SIZE, BUTTON_SIZE)
y += ROW_HEIGHT + ROW_GAP
func natural_height() -> float:
return 46.0 + float(_rows.size()) * (ROW_HEIGHT + ROW_GAP)
func refresh() -> void:
if _rows.is_empty():
return
var planned: int = 0
for vessel_type in GameState.vessel_targets:
planned += int(GameState.vessel_targets[vessel_type])
_header.text = "STANDING ORDERS %d VESSELS" % planned
for row in _rows:
var vessel_type: String = String(row.type)
var want: int = GameState.vessel_target(vessel_type)
var have: int = GameState.vessel_census(vessel_type)
var building: int = GameState.vessel_count_on_order(vessel_type)
row.count.text = "%d/%d" % [have, want]
# Where they are, not merely how many: a plan can look unmet while
# every vessel it asked for is sitting at an outpost.
var parts: PackedStringArray = PackedStringArray()
var flying: int = GameState.vessel_count_in_fleet(vessel_type)
var stored: int = GameState.vessel_count_in_hangar(vessel_type)
var stationed: int = GameState.vessel_count_in_garrisons(vessel_type)
var moving: int = GameState.vessel_count_in_transit(vessel_type)
if flying > 0:
parts.append("%d flying" % flying)
if stored > 0:
parts.append("%d stored" % stored)
if stationed > 0:
parts.append("%d stationed" % stationed)
if moving > 0:
parts.append("%d moving" % moving)
if building > 0:
parts.append("%d building" % building)
row.note.text = ", ".join(parts)
var unlocked: bool = GameState.vessel_is_unlocked(vessel_type)
row.name.modulate.a = 1.0 if unlocked else 0.45
row.minus.enabled = unlocked and want > 0
row.plus.enabled = unlocked
row.minus.queue_redraw()
row.plus.queue_redraw()

View file

@ -96,6 +96,13 @@ func vessel_types() -> PackedStringArray:
func _sync_composition_state() -> void: func _sync_composition_state() -> void:
# #106: the Planning Center counts vessels across the whole game, and
# the fleet is the one place it cannot see - the ships are scene nodes
# and GameState outlives the scene. Pushed here rather than polled,
# alongside the cargo capacity and distorder count that already are,
# and for the same reason: composition changes are authoritative, and
# this is the one function every one of them goes through.
GameState.fleet_vessel_types = vessel_types()
GameState.sync_fleet_cargo_capacity(vessel_types()) GameState.sync_fleet_cargo_capacity(vessel_types())
# Composition changes are authoritative; scene teardown is not. Keeping # Composition changes are authoritative; scene teardown is not. Keeping
# this out of SpeedFader prevents an outgoing scene's empty fleet from # this out of SpeedFader prevents an outgoing scene's empty fleet from

View file

@ -23,6 +23,7 @@
[ext_resource type="Script" path="res://ui/WaveAlert.gd" id="32"] [ext_resource type="Script" path="res://ui/WaveAlert.gd" id="32"]
[ext_resource type="Script" path="res://ui/GarrisonPanel.gd" id="33"] [ext_resource type="Script" path="res://ui/GarrisonPanel.gd" id="33"]
[ext_resource type="Script" path="res://vessels/GarrisonField.gd" id="34"] [ext_resource type="Script" path="res://vessels/GarrisonField.gd" id="34"]
[ext_resource type="Script" path="res://ui/PlanningPanel.gd" id="35"]
[ext_resource type="Script" path="res://logistics/TransporterField.gd" id="23"] [ext_resource type="Script" path="res://logistics/TransporterField.gd" id="23"]
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"] [ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
[ext_resource type="Script" path="res://logistics/VesselDeliveryField.gd" id="26"] [ext_resource type="Script" path="res://logistics/VesselDeliveryField.gd" id="26"]
@ -766,6 +767,48 @@ script = ExtResource("19")
amount = 30 amount = 30
digit_height = 40.0 digit_height = 40.0
[node name="PlanningButton" type="Control" parent="UI/BuildMenu"]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -64
offset_right = 64
offset_top = 1090
offset_bottom = 1218
script = ExtResource("10")
icon = 41
glow_color = Color(0.62, 0.74, 0.92, 1)
[node name="PlanningLabel" type="Label" parent="UI/BuildMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -150
offset_right = 150
offset_top = 1224
offset_bottom = 1290
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Planning Center"
horizontal_alignment = 1
[node name="PlanningCost" type="Control" parent="UI/BuildMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -60
offset_right = 60
offset_top = 1294
offset_bottom = 1346
script = ExtResource("19")
amount = 70
digit_height = 40.0
[node name="CloseButton" type="Control" parent="UI/BuildMenu"] [node name="CloseButton" type="Control" parent="UI/BuildMenu"]
anchor_left = 1.0 anchor_left = 1.0
anchor_right = 1.0 anchor_right = 1.0
@ -2107,6 +2150,63 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
color = Color(0, 0, 0, 0) color = Color(0, 0, 0, 0)
[node name="PlanningMenu" type="Control" parent="UI" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Backdrop" type="ColorRect" parent="UI/PlanningMenu"]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="UI/PlanningMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 20.0
offset_right = -200.0
offset_bottom = 82.0
text = "Planning Center"
horizontal_alignment = 1
[node name="Panel" type="Control" parent="UI/PlanningMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 40.0
offset_top = 120.0
offset_right = -40.0
offset_bottom = -180.0
anchor_bottom = 1.0
script = ExtResource("35")
[node name="StatusLabel" type="Label" parent="UI/PlanningMenu"]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32.0
offset_top = -160.0
offset_right = -32.0
offset_bottom = -24.0
theme_override_font_sizes/font_size = 28
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="CloseButton" type="Control" parent="UI/PlanningMenu"]
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("10")
icon = 1
glow_color = Color(0.62, 0.74, 0.92, 1)
[node name="GarrisonMenu" type="Control" parent="UI" groups=["modal_menus"]] [node name="GarrisonMenu" type="Control" parent="UI" groups=["modal_menus"]]
visible = false visible = false
mouse_filter = 2 mouse_filter = 2