#102/#103 (#97d/#97e): the outpost, and the garrison you compose for it
Landed together because an outpost you cannot crew is decoration, and a garrison menu with no building to hang it off is nothing at all. #102, the building. Researched at "Forward Bases", two prerequisites deep and cheaper than any mid-game structure - the ticket asks for early, and the answer to waves attacking your buildings should arrive at about the time the waves can hurt them. It is drawn as a watch mast on a tripod with two open berths, deliberately the least machinery of any structure here: it makes nothing and refines nothing, it holds a position. 120hp, more than a crafter, because being shot at is its job. #103, the garrison. Two separate things live on its record, and conflating them was the first thing to get wrong: the ORDER, what the player asked for, and the CREW, who is actually there. This menu edits the order only; #104 fills the crew. Keeping the order as a wish is what lets an outpost ask for a hull nobody has built yet and wait for it, rather than the request evaporating - and it is why the panel shows "here / ordered" instead of one number. "Only ships that attack can be assigned" is expressed as capabilities rather than a list of types, so a new armed vessel is eligible the day it is added and an unarmed one never is by accident. Today that means the interceptor and the laser vessel; a shield, a hauler and an engine block cannot hold a position however much one might want them to. Two berths by default, and Garrison Bays raises it at outposts ALREADY standing - the same shape as freight_yards for hubs, because a bonus that only reached new buildings would mean demolishing and rebuilding to collect it. The panel builds its own rows from the catalog rather than from nodes placed in the scene, so adding an armed vessel later needs no scene surgery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3debcd7edd
commit
3c632276af
12 changed files with 909 additions and 1 deletions
|
|
@ -306,6 +306,9 @@ const SHIP_COST: int = 6
|
|||
const BUILDING_MOTHERSHIP: String = "mothership"
|
||||
const BUILDING_SHIP_CRAFTER: String = "ship_crafter"
|
||||
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"
|
||||
# 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"
|
||||
|
|
@ -362,6 +365,23 @@ const SKILL_TREE: Array = [
|
|||
"x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "research_points": 1,
|
||||
"desc": "Build command centers, where the fleet formation is arranged.",
|
||||
},
|
||||
{
|
||||
# Early on purpose (#97: "pretty early in the tech tree"). It hangs off
|
||||
# coordination rather than off a deep branch, so the answer to waves
|
||||
# attacking your structures is available at about the time the waves
|
||||
# start being able to hurt them.
|
||||
"id": "forward_bases", "name": "Forward Bases", "cost": 30, "requires": "coordination",
|
||||
"x": 0.66, "unlocks": BUILDING_OUTPOST, "research_points": 1,
|
||||
"desc": "Build outposts: forward positions vessels can be stationed at, to hold ground you are not standing on.",
|
||||
},
|
||||
{
|
||||
# The same shape as freight_yards for hubs: a permanent bonus that
|
||||
# applies to outposts already standing, not only to new ones, so
|
||||
# researching it never means demolishing and rebuilding.
|
||||
"id": "garrison_bays", "name": "Garrison Bays", "cost": 110, "requires": "forward_bases",
|
||||
"x": 0.66, "outpost_bonus": 1, "research_points": 2,
|
||||
"desc": "Permanent: +1 berth at every outpost, including ones already built.",
|
||||
},
|
||||
{
|
||||
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
||||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1,
|
||||
|
|
@ -728,6 +748,138 @@ func destroy_building(id: int) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
# --- outpost garrisons (#103) ----------------------------------------------
|
||||
#
|
||||
# Two separate things live on an outpost's record, and conflating them was
|
||||
# the first thing to get wrong here:
|
||||
#
|
||||
# garrison what the player ASKED for - {vessel_type: count}. Set in
|
||||
# the outpost's own menu and otherwise untouched.
|
||||
# garrison_crew who is actually THERE - a list of vessel types. #104
|
||||
# fills it from the mothership; nothing else writes it.
|
||||
#
|
||||
# Keeping the order as a wish rather than as an inventory is what lets an
|
||||
# outpost ask for a vessel nobody has yet and simply wait, instead of the
|
||||
# request evaporating.
|
||||
|
||||
const GARRISON_BASE_SLOTS: int = 2
|
||||
# Which vessels can hold a position. #97: "Only ships that attack can be
|
||||
# assigned" - so a shield vessel, a hauler or an engine block cannot, however
|
||||
# much the player might want the shield out there. Written as capabilities
|
||||
# rather than as a list of types, so a new armed vessel is eligible the day
|
||||
# it is added and an unarmed one never is by accident.
|
||||
const GARRISON_CAPABILITIES: PackedStringArray = ["weapon", "laser"]
|
||||
|
||||
|
||||
func outpost_slot_capacity() -> int:
|
||||
var n: int = GARRISON_BASE_SLOTS
|
||||
for id in unlocked:
|
||||
n += int(skill_node(id).get("outpost_bonus", 0))
|
||||
return n
|
||||
|
||||
|
||||
func vessel_can_garrison(vessel_type: String) -> bool:
|
||||
var definition: Resource = vessel_definition(vessel_type)
|
||||
if definition == null or not definition.fleet_eligible:
|
||||
return false
|
||||
for capability in GARRISON_CAPABILITIES:
|
||||
if capability in definition.capabilities:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func garrison_vessel_types() -> PackedStringArray:
|
||||
var out: PackedStringArray = PackedStringArray()
|
||||
for definition in VESSEL_DEFINITIONS.all():
|
||||
if vessel_can_garrison(String(definition.definition_id)):
|
||||
out.append(String(definition.definition_id))
|
||||
return out
|
||||
|
||||
|
||||
func is_outpost(building: Dictionary) -> bool:
|
||||
return building_has_capability(building, "garrison")
|
||||
|
||||
|
||||
func garrison_desired(id: int) -> Dictionary:
|
||||
var b: Dictionary = find_building(id)
|
||||
if b.is_empty() or not is_outpost(b):
|
||||
return {}
|
||||
if not b.has("garrison"):
|
||||
b.garrison = {}
|
||||
return b.garrison
|
||||
|
||||
|
||||
func garrison_desired_total(id: int) -> int:
|
||||
var n: int = 0
|
||||
for count in garrison_desired(id).values():
|
||||
n += int(count)
|
||||
return n
|
||||
|
||||
|
||||
func garrison_desired_of(id: int, vessel_type: String) -> int:
|
||||
return int(garrison_desired(id).get(vessel_type, 0))
|
||||
|
||||
|
||||
# Returns the count actually set, so the caller can show what happened when
|
||||
# the request was clamped rather than silently doing something else.
|
||||
func set_garrison_desired(id: int, vessel_type: String, count: int) -> int:
|
||||
var b: Dictionary = find_building(id)
|
||||
if b.is_empty() or not is_outpost(b) or not vessel_can_garrison(vessel_type):
|
||||
return 0
|
||||
var wanted: Dictionary = garrison_desired(id)
|
||||
var others: int = garrison_desired_total(id) - int(wanted.get(vessel_type, 0))
|
||||
var room: int = maxi(outpost_slot_capacity() - others, 0)
|
||||
var final_count: int = clampi(count, 0, room)
|
||||
if final_count <= 0:
|
||||
wanted.erase(vessel_type)
|
||||
else:
|
||||
wanted[vessel_type] = final_count
|
||||
return final_count
|
||||
|
||||
|
||||
func adjust_garrison_desired(id: int, vessel_type: String, delta: int) -> int:
|
||||
return set_garrison_desired(id, vessel_type,
|
||||
garrison_desired_of(id, vessel_type) + delta)
|
||||
|
||||
|
||||
func garrison_crew(id: int) -> Array:
|
||||
var b: Dictionary = find_building(id)
|
||||
if b.is_empty() or not is_outpost(b):
|
||||
return []
|
||||
if not b.has("garrison_crew"):
|
||||
b.garrison_crew = []
|
||||
return b.garrison_crew
|
||||
|
||||
|
||||
func garrison_crew_of(id: int, vessel_type: String) -> int:
|
||||
var n: int = 0
|
||||
for crewed in garrison_crew(id):
|
||||
if String(crewed) == vessel_type:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# What this outpost is still short of, as {vessel_type: count}. The one
|
||||
# question #104 has to answer every tick, so it is asked here rather than
|
||||
# recomputed by whoever needs it.
|
||||
func garrison_shortfall(id: int) -> Dictionary:
|
||||
var out: Dictionary = {}
|
||||
for vessel_type in garrison_desired(id):
|
||||
var missing: int = int(garrison_desired(id)[vessel_type]) \
|
||||
- garrison_crew_of(id, String(vessel_type))
|
||||
if missing > 0:
|
||||
out[vessel_type] = missing
|
||||
return out
|
||||
|
||||
|
||||
func outpost_ids() -> Array:
|
||||
var out: Array = []
|
||||
for b in buildings:
|
||||
if is_outpost(b):
|
||||
out.append(int(b.id))
|
||||
return out
|
||||
|
||||
|
||||
func building_has_capability(building: Dictionary, capability: String) -> bool:
|
||||
if building.is_empty():
|
||||
return false
|
||||
|
|
|
|||
Loading…
Reference in a new issue