Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
472de1d93e
commit
097e17f7aa
18 changed files with 567 additions and 12 deletions
130
GameState.gd
130
GameState.gd
|
|
@ -32,6 +32,7 @@ var explored_chunks: Dictionary = {}
|
|||
|
||||
const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID
|
||||
const RESOURCE_GAS: String = RESOURCE_DEFINITIONS.GAS_ID
|
||||
const RESOURCE_ENERGY: String = RESOURCE_DEFINITIONS.ENERGY_ID
|
||||
const RESOURCE_TYPES: PackedStringArray = RESOURCE_DEFINITIONS.ORDER
|
||||
|
||||
# All resource-bearing locations use the same container. The old public
|
||||
|
|
@ -190,11 +191,15 @@ const BUILDING_EXTRACTOR: String = "extractor"
|
|||
const BUILDING_TRANSPORT_HUB: String = "transport_hub"
|
||||
# Where vessel types are researched and new ones unlocked (#5).
|
||||
const BUILDING_SHIP_LAB: String = "ship_lab"
|
||||
# Converts gas into energy, both stored/moved through the building's own
|
||||
# inventory exactly like every other building (#54).
|
||||
const BUILDING_GAS_BURNER: String = "gas_burner"
|
||||
|
||||
const COMMAND_CENTER_COST: int = 15
|
||||
const EXTRACTOR_COST: int = 25
|
||||
const TRANSPORT_HUB_COST: int = 30
|
||||
const SHIP_LAB_COST: int = 35
|
||||
const GAS_BURNER_COST: int = 30
|
||||
|
||||
var buildings: Array = []
|
||||
|
||||
|
|
@ -242,6 +247,11 @@ const SKILL_TREE: Array = [
|
|||
"x": 0.18, "hub_bonus": 1,
|
||||
"desc": "Permanent: +1 transporter stationed at every hub, including ones already built.",
|
||||
},
|
||||
{
|
||||
"id": "power_systems", "name": "Power Systems", "cost": 45, "requires": "freight",
|
||||
"x": 0.50, "unlocks": BUILDING_GAS_BURNER,
|
||||
"desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.",
|
||||
},
|
||||
{
|
||||
"id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination",
|
||||
"x": 0.82, "fleet_bonus": 1,
|
||||
|
|
@ -1047,6 +1057,46 @@ func _extractor_inventory(extractor: Dictionary) -> RefCounted:
|
|||
return inventory
|
||||
|
||||
|
||||
# --- gas burners (#54) -----------------------------------------------------
|
||||
#
|
||||
# Converts gas into energy at the building's own site, simulated here
|
||||
# alongside extractor mining and production so it keeps running whether or
|
||||
# not the player is nearby. Deliberately withdraws only from the building's
|
||||
# OWN inventory, never the bank - a burner with no local gas simply stalls,
|
||||
# exactly like a lab or crafter waiting on materials, rather than conjuring
|
||||
# energy for free.
|
||||
|
||||
const GAS_BURN_RATE: float = 1.0 # gas/second
|
||||
const ENERGY_PER_GAS: int = 2
|
||||
# Local gas reserve a burner tries to keep topped up. Transporters treat
|
||||
# falling short of this the same way they treat a queued production/research
|
||||
# order (see _dispatch_transporters()'s gas_burner_gas_deficit() use).
|
||||
const GAS_BURNER_BUFFER: int = 40
|
||||
|
||||
|
||||
func gas_burner_gas_deficit(id: int) -> int:
|
||||
var b: Dictionary = find_building(id)
|
||||
if not building_has_capability(b, "gas_burner"):
|
||||
return 0
|
||||
return maxi(0, GAS_BURNER_BUFFER - building_resource(id, RESOURCE_GAS))
|
||||
|
||||
|
||||
func _advance_gas_burners(delta: float) -> void:
|
||||
for b in buildings:
|
||||
if not building_has_capability(b, "gas_burner"):
|
||||
continue
|
||||
var inventory: RefCounted = _inventory_for_record(b)
|
||||
if inventory.amount(RESOURCE_GAS) <= 0:
|
||||
continue # stalled - no local gas to burn
|
||||
var progress: float = float(b.get("gas_burn_progress", 0.0)) + delta * GAS_BURN_RATE
|
||||
var whole: int = mini(int(floor(progress)), inventory.amount(RESOURCE_GAS))
|
||||
if whole > 0:
|
||||
inventory.withdraw(RESOURCE_GAS, whole)
|
||||
inventory.deposit(RESOURCE_ENERGY, whole * ENERGY_PER_GAS)
|
||||
progress -= whole
|
||||
b.gas_burn_progress = progress
|
||||
|
||||
|
||||
# Planet conquest: a planet must have every one of its capture points held
|
||||
# before an extractor can be built there (see BuildController's placement
|
||||
# gates). Keyed by the same stable planet_id as `extractors`, entirely
|
||||
|
|
@ -1117,6 +1167,7 @@ func _process(delta: float) -> void:
|
|||
_advance_production(delta)
|
||||
_advance_lab_research(delta)
|
||||
_advance_vessel_deliveries(delta)
|
||||
_advance_gas_burners(delta)
|
||||
for e in extractors:
|
||||
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
|
||||
var whole: int = int(floor(float(e.mining_progress)))
|
||||
|
|
@ -1166,6 +1217,12 @@ const TRANSPORTER_TO_BUILDING: String = "to_building"
|
|||
const TRANSPORTER_RETURNING: String = "returning"
|
||||
const TRANSPORTER_TO_EXTRACTOR: String = "to_extractor"
|
||||
const TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR: String = "extractor_to_mothership"
|
||||
# Collecting an output resource (energy) directly from a regular building's
|
||||
# own inventory - a gas burner, not an extractor - and banking it. Reuses
|
||||
# `target_id` exactly like the ordinary supply path does, rather than
|
||||
# `extractor_index`: the source is a building, already addressable that way.
|
||||
const TRANSPORTER_TO_BUILDING_PICKUP: String = "to_building_pickup"
|
||||
const TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING: String = "building_to_mothership"
|
||||
|
||||
var transporters: Array = []
|
||||
|
||||
|
|
@ -1227,8 +1284,23 @@ func _begin_leg(t: Dictionary, from: Vector2, to: Vector2, state: String) -> voi
|
|||
|
||||
|
||||
func _transporter_inbound_to(building_id: int) -> bool:
|
||||
# Delivery-purpose states only (#54): a building can have BOTH a
|
||||
# transporter delivering it something (gas, for a burner) and a
|
||||
# separate one collecting something from it (energy) at once, since
|
||||
# they are two different transporters and two different legs. Matching
|
||||
# on target_id alone, regardless of state, would make either purpose
|
||||
# block the other from ever being dispatched to the same building.
|
||||
for t in transporters:
|
||||
if t.state != TRANSPORTER_IDLE and int(t.target_id) == building_id:
|
||||
if (t.state == TRANSPORTER_TO_MOTHERSHIP or t.state == TRANSPORTER_TO_BUILDING) \
|
||||
and int(t.target_id) == building_id:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _transporter_inbound_for_pickup(building_id: int) -> bool:
|
||||
for t in transporters:
|
||||
if (t.state == TRANSPORTER_TO_BUILDING_PICKUP or t.state == TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING) \
|
||||
and int(t.target_id) == building_id:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
|
@ -1253,8 +1325,14 @@ func _dispatch_transporters() -> void:
|
|||
for b in buildings:
|
||||
var needed_resource := ""
|
||||
for resource_type in resource_types():
|
||||
if outstanding_resource_cost(int(b.id), resource_type) > 0 \
|
||||
and bank_inventory.amount(resource_type) > 0:
|
||||
# A gas burner's ongoing gas deficit is folded in alongside
|
||||
# queued production/research costs (#54) - both are just "this
|
||||
# building wants more of resource_type", so the one dispatch
|
||||
# path already below serves both without any new transporter
|
||||
# state.
|
||||
var need: int = maxi(outstanding_resource_cost(int(b.id), resource_type),
|
||||
gas_burner_gas_deficit(int(b.id)) if resource_type == RESOURCE_GAS else 0)
|
||||
if need > 0 and bank_inventory.amount(resource_type) > 0:
|
||||
needed_resource = resource_type
|
||||
break
|
||||
if needed_resource == "":
|
||||
|
|
@ -1306,6 +1384,32 @@ func _dispatch_transporters() -> void:
|
|||
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
|
||||
TRANSPORTER_TO_EXTRACTOR)
|
||||
|
||||
# Collecting a gas burner's energy output, last: queued deliveries and
|
||||
# extractor collection both outrank it, same "supply what's needed
|
||||
# before what's merely ready" ordering _dispatch_transporters() already
|
||||
# holds for extractors over queued orders (f5c60f8).
|
||||
for b in buildings:
|
||||
if not building_has_capability(b, "gas_burner"):
|
||||
continue
|
||||
if building_resource(int(b.id), RESOURCE_ENERGY) < load or _transporter_inbound_for_pickup(int(b.id)):
|
||||
continue
|
||||
var chosen_pickup: Dictionary = {}
|
||||
var best_pickup_distance: float = INF
|
||||
for t in transporters:
|
||||
if t.state != TRANSPORTER_IDLE:
|
||||
continue
|
||||
var d: float = transporter_position(t).distance_squared_to(b.position)
|
||||
if d < best_pickup_distance:
|
||||
best_pickup_distance = d
|
||||
chosen_pickup = t
|
||||
if chosen_pickup.is_empty():
|
||||
return
|
||||
chosen_pickup.target_id = int(b.id)
|
||||
chosen_pickup.resource_type = RESOURCE_ENERGY
|
||||
chosen_pickup.extractor_index = -1
|
||||
_begin_leg(chosen_pickup, transporter_position(chosen_pickup), b.position,
|
||||
TRANSPORTER_TO_BUILDING_PICKUP)
|
||||
|
||||
|
||||
func _advance_transporters(delta: float) -> void:
|
||||
_sync_hub_transporters()
|
||||
|
|
@ -1331,7 +1435,8 @@ func _arrive(t: Dictionary) -> void:
|
|||
TRANSPORTER_TO_MOTHERSHIP:
|
||||
var target: Dictionary = find_building(int(t.target_id))
|
||||
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
||||
var want: int = outstanding_resource_cost(int(t.target_id), resource_type)
|
||||
var want: int = maxi(outstanding_resource_cost(int(t.target_id), resource_type),
|
||||
gas_burner_gas_deficit(int(t.target_id)) if resource_type == RESOURCE_GAS else 0)
|
||||
var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit())
|
||||
if take <= 0 or target.is_empty():
|
||||
# Nothing to carry, or the building is gone. Go home empty.
|
||||
|
|
@ -1374,6 +1479,23 @@ func _arrive(t: Dictionary) -> void:
|
|||
t.carrying = 0
|
||||
t.extractor_index = -1
|
||||
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
||||
TRANSPORTER_TO_BUILDING_PICKUP:
|
||||
var source: Dictionary = find_building(int(t.target_id))
|
||||
var resource_type: String = String(t.get("resource_type", RESOURCE_ENERGY))
|
||||
var take: int = 0 if source.is_empty() \
|
||||
else mini(building_resource(int(t.target_id), resource_type), transporter_load_limit())
|
||||
if take <= 0 or source.is_empty():
|
||||
t.target_id = 0
|
||||
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
||||
return
|
||||
_inventory_for_record(source).withdraw(resource_type, take)
|
||||
t.carrying = take
|
||||
_begin_leg(t, here, get_mothership_position(), TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING)
|
||||
TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING:
|
||||
bank_inventory.deposit(String(t.get("resource_type", RESOURCE_ENERGY)), int(t.carrying))
|
||||
t.carrying = 0
|
||||
t.target_id = 0
|
||||
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
||||
TRANSPORTER_RETURNING:
|
||||
t.state = TRANSPORTER_IDLE
|
||||
t.progress = 1.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue