#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:
parent
bd0cc842ff
commit
0c595d775d
12 changed files with 883 additions and 16 deletions
|
|
@ -53,6 +53,8 @@ enum Icon {
|
|||
# #102: appended, like everything above - inserting anywhere earlier
|
||||
# renumbers every icon int already stored in every scene.
|
||||
OUTPOST,
|
||||
# #106: appended, same rule.
|
||||
PLANNING_CENTER,
|
||||
}
|
||||
|
||||
# Size the glyphs were originally drawn against. Every glyph dimension is
|
||||
|
|
@ -296,6 +298,8 @@ func _draw() -> void:
|
|||
_draw_distorder_vessel(center, pulse, s)
|
||||
Icon.OUTPOST:
|
||||
_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:
|
||||
|
|
@ -664,6 +668,26 @@ func _draw_ship_lab_icon(center: Vector2, pulse: float, s: float) -> void:
|
|||
_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:
|
||||
# A watchtower with two vessels holding station off it. The hub's
|
||||
# octagon already means "things are stationed here and go out from
|
||||
|
|
|
|||
150
ui/PlanningPanel.gd
Normal file
150
ui/PlanningPanel.gd
Normal 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()
|
||||
Loading…
Reference in a new issue