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:
Loïc Wisniewski 2026-08-16 12:10:31 +02:00
commit 097e17f7aa
18 changed files with 567 additions and 12 deletions

View file

@ -31,6 +31,10 @@ enum Icon {
MINUS, PLUS,
SHIELD_VESSEL,
GAS_BOMBER, GAS_BOMB,
# Appended, not inserted alongside the other building icons above -
# inserting there would silently renumber every icon int already
# stored in existing scenes (#54).
GAS_BURNER,
}
# Size the glyphs were originally drawn against. Every glyph dimension is
@ -221,6 +225,8 @@ func _draw() -> void:
_draw_gas_bomber(center, pulse, s)
Icon.GAS_BOMB:
_draw_gas_bomb(center, pulse, s)
Icon.GAS_BURNER:
_draw_gas_burner_icon(center, pulse, s)
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
@ -266,6 +272,26 @@ func _draw_gas_bomb(center: Vector2, pulse: float, s: float) -> void:
_active_color(), pulse * 0.85, core_color)
func _draw_gas_burner_icon(center: Vector2, pulse: float, s: float) -> void:
# A hexagonal chamber with a hot core and vents, miniaturised from the
# building's own silhouette (BuildingVisualBase._draw_gas_burner_static)
# so the menu icon reads as the same structure being placed.
var outer: PackedVector2Array = PackedVector2Array()
var core: PackedVector2Array = PackedVector2Array()
for i in range(6):
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
outer.append(center + Vector2(cos(a), sin(a)) * 11.0 * s)
core.append(center + Vector2(cos(a), sin(a)) * 5.5 * s)
NeonDraw.draw_glowing_polygon(self, outer, _active_color(), pulse, core_color)
NeonDraw.draw_glowing_polygon(self, core, _active_color(), pulse * 0.85, core_color)
for i in range(4):
var a: float = (float(i) / 4.0) * TAU + PI / 4.0
var dir: Vector2 = Vector2(cos(a), sin(a))
NeonDraw.draw_glowing_line(self, center + dir * 11.0 * s, center + dir * 15.5 * s,
_active_color(), pulse, 0.35 * s, core_color)
func _draw_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void:
var r: float = 9.0 * s
var tip: Vector2 = center + Vector2(0, r * direction)