#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
|
|
@ -20,6 +20,10 @@ const WORLD_GEN = preload("res://world/WorldGen.gd")
|
|||
var has_return_position: bool = false
|
||||
var surviving_ship_count: int = STARTING_SHIP_COUNT
|
||||
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_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
|
||||
# it is the answer to waves going after what you have built.
|
||||
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
|
||||
# planet's surface and tracked separately (see `extractors`).
|
||||
const BUILDING_EXTRACTOR: String = "extractor"
|
||||
|
|
@ -382,6 +388,16 @@ const SKILL_TREE: Array = [
|
|||
"x": 0.66, "outpost_bonus": 1, "research_points": 2,
|
||||
"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",
|
||||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1,
|
||||
|
|
@ -556,6 +572,7 @@ func reset_new_game() -> void:
|
|||
has_return_position = false
|
||||
surviving_ship_count = STARTING_SHIP_COUNT
|
||||
surviving_vessel_types = PackedStringArray()
|
||||
fleet_vessel_types = PackedStringArray()
|
||||
return_world_position = Vector2.ZERO
|
||||
return_camera_zoom = 1.0
|
||||
planet_name = ""
|
||||
|
|
@ -576,6 +593,7 @@ func reset_new_game() -> void:
|
|||
extractors.clear()
|
||||
transporters.clear()
|
||||
planet_capture.clear()
|
||||
vessel_targets.clear()
|
||||
reset_waves()
|
||||
reset_progression()
|
||||
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:
|
||||
var lab: Dictionary = find_building(lab_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_vessel_deliveries(delta)
|
||||
_advance_garrison_dispatch()
|
||||
_advance_planning()
|
||||
_advance_resource_converters(delta)
|
||||
_advance_distorder_drain(delta)
|
||||
for e in extractors:
|
||||
|
|
|
|||
Loading…
Reference in a new issue