Garrison laser vessels and gas bombers, on a shared ability seam
#105 shipped with interceptors as the only garrisonable vessel, and the reason was a design fault rather than a decision: firing the laser and the gas bomb lived in the player's fleet controllers, so a stationed laser would have held its position and never shot. The shot is now a thing in the world rather than something a controller draws. LaserBeamEffect and GasBombEffect are self-contained, self-animating and self-freeing, added to the tree root - the same shape EnemyBigLaserBeam and ArtilleryShell already had on the enemy side - and they outlive the vessel that fired them. Above them sits one seam, on VesselCooldownFillComponent: ability_range(), ability_ammo_resource(), ability_ammo_cost() and fire_ability(). A beam and a bomb are now the same shape of thing to whoever pulls the trigger, so the garrison AI never names an ability - the test asserts that literally, by reading the AI's own source. A third armed vessel needs to implement the seam and nothing else. Ammunition is the CALLER's business, deliberately: the fleet spends from its hold, a garrison from its outpost's store, and neither concern belongs in the component. A garrison with an empty magazine holds fire rather than falling back on the fleet's hold. Outpost storage: 50 gas per stationed bomber and 50 energy per stationed laser, off the VESSEL definitions, so an outpost never learns which vessel needs what. It is a capacity, not a stock - hub transporters fill it exactly as they fill a gas burner's buffer, because building_logistics_deficit now takes the larger of the definition's static buffer and the garrison's magazine. Every other resource is capped at zero: an outpost is not a depot, and ore parked at one has nothing to do there and no transporter coming for it. Also restores two properties the Godot editor dropped from GasBurnerBuildingDefinition.tres when it re-saved the file: collected_outputs and capabilities. Without the first, nothing ever collects the burner's energy - test_gas_burner caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0c595d775d
commit
b50597b757
16 changed files with 809 additions and 209 deletions
|
|
@ -221,6 +221,19 @@ func _inventory_for_record(record: Dictionary) -> RefCounted:
|
|||
if inventory == null:
|
||||
inventory = INVENTORY.new()
|
||||
record.inventory = inventory
|
||||
# An outpost holds exactly what its garrison can use, and the garrison
|
||||
# changes. Re-applied on every access rather than when the crew changes,
|
||||
# because there is no single place a crew change goes through - a vessel
|
||||
# arriving, one being released and one being shot down are three.
|
||||
if is_outpost(record):
|
||||
# Every resource gets a ceiling, not only the ones a magazine wants:
|
||||
# an outpost is not a depot, and ore parked at one has nothing to do
|
||||
# there and no transporter coming for it.
|
||||
var capacities: Dictionary = {}
|
||||
var magazines: Dictionary = garrison_magazines(int(record.id))
|
||||
for resource_type in resource_types():
|
||||
capacities[resource_type] = int(magazines.get(resource_type, 0))
|
||||
inventory.resource_capacities = capacities
|
||||
# Compatibility migration for state made before #33. Erase each legacy
|
||||
# field once absorbed so it cannot drift out of step with the inventory.
|
||||
if record.has("stock"):
|
||||
|
|
@ -787,13 +800,14 @@ const GARRISON_BASE_SLOTS: int = 2
|
|||
# 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.
|
||||
#
|
||||
# "weapon" only, and the laser deliberately left out (#105). A garrison
|
||||
# vessel has to fight WITHOUT the player, and firing the laser lives in
|
||||
# LaserController - a fleet ability driven by the laser button - rather than
|
||||
# in VesselLaserComponent, which only tracks its cooldown and ammunition. A
|
||||
# garrisoned laser vessel would therefore hold its position and never shoot.
|
||||
# Offering a vessel that cannot fight is worse than a short list.
|
||||
const GARRISON_CAPABILITIES: PackedStringArray = ["weapon"]
|
||||
# #105 shipped with "weapon" only, because firing the laser and the gas bomb
|
||||
# lived in the player's controllers rather than in the vessels' own
|
||||
# components - a garrisoned laser would have held its position and never
|
||||
# shot. Both effects now live in the world as LaserBeamEffect/GasBombEffect
|
||||
# and both components expose the same fire_ability() seam, so a garrison can
|
||||
# pull either trigger and the list is no longer a apology for a missing
|
||||
# feature.
|
||||
const GARRISON_CAPABILITIES: PackedStringArray = ["weapon", "laser", "gas_bomber"]
|
||||
|
||||
|
||||
func outpost_slot_capacity() -> int:
|
||||
|
|
@ -876,6 +890,56 @@ func garrison_crew(id: int) -> Array:
|
|||
return b.garrison_crew
|
||||
|
||||
|
||||
# --- what a garrison keeps beside it ---------------------------------------
|
||||
#
|
||||
# An outpost stores ammunition for the vessels actually stationed there: 50
|
||||
# gas for each gas bomber, 50 energy for each laser. The numbers come off the
|
||||
# VESSEL definitions, so the outpost never learns which vessel needs what -
|
||||
# add an armed vessel with a magazine and its outpost storage follows.
|
||||
#
|
||||
# This is a CAPACITY, not a stock. Hub transporters fill it the same way they
|
||||
# fill a gas burner's buffer (see building_logistics_deficit), and the
|
||||
# garrison AI spends from it.
|
||||
|
||||
func garrison_magazine_capacity(id: int, resource_type: String) -> int:
|
||||
var total: int = 0
|
||||
for vessel_type in garrison_crew(id):
|
||||
var definition: Resource = vessel_definition(String(vessel_type))
|
||||
if definition == null:
|
||||
continue
|
||||
if String(definition.garrison_ammo_resource) != resource_type:
|
||||
continue
|
||||
total += int(definition.garrison_ammo_capacity)
|
||||
return total
|
||||
|
||||
|
||||
# Every resource this outpost wants held, as {resource_type: capacity}.
|
||||
func garrison_magazines(id: int) -> Dictionary:
|
||||
var out: Dictionary = {}
|
||||
for vessel_type in garrison_crew(id):
|
||||
var definition: Resource = vessel_definition(String(vessel_type))
|
||||
if definition == null:
|
||||
continue
|
||||
var resource_type: String = String(definition.garrison_ammo_resource)
|
||||
if resource_type == "" or int(definition.garrison_ammo_capacity) <= 0:
|
||||
continue
|
||||
out[resource_type] = int(out.get(resource_type, 0)) \
|
||||
+ int(definition.garrison_ammo_capacity)
|
||||
return out
|
||||
|
||||
|
||||
# Spends from the outpost's own store. Returns false when it cannot pay in
|
||||
# full - a half-priced shot would be worse than no shot.
|
||||
func spend_garrison_ammo(id: int, resource_type: String, amount: int) -> bool:
|
||||
if amount <= 0:
|
||||
return true
|
||||
var inventory: RefCounted = building_inventory(id)
|
||||
if inventory == null or inventory.amount(resource_type) < amount:
|
||||
return false
|
||||
inventory.withdraw(resource_type, amount)
|
||||
return true
|
||||
|
||||
|
||||
func garrison_crew_of(id: int, vessel_type: String) -> int:
|
||||
var n: int = 0
|
||||
for crewed in garrison_crew(id):
|
||||
|
|
@ -1862,10 +1926,14 @@ func _extractor_inventory(extractor: Dictionary) -> RefCounted:
|
|||
|
||||
func building_logistics_deficit(id: int, resource_type: String) -> int:
|
||||
var building: Dictionary = find_building(id)
|
||||
var target: int = 0
|
||||
var profile: Resource = building_logistics_profile(building)
|
||||
if profile == null:
|
||||
return 0
|
||||
var target: int = int(profile.input_buffers.get(resource_type, 0))
|
||||
if profile != null:
|
||||
target = int(profile.input_buffers.get(resource_type, 0))
|
||||
# An outpost has no logistics profile at all - what it wants held depends
|
||||
# on who is stationed there, which is not something a definition can
|
||||
# state. Taking the larger of the two keeps the static case untouched.
|
||||
target = maxi(target, garrison_magazine_capacity(id, resource_type))
|
||||
return maxi(0, target - building_resource(id, resource_type))
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue