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
|
|
@ -490,14 +490,21 @@ func _planet_under_fleet() -> Node2D:
|
|||
# Grid geometry, shared with the layout below. Rows of two, and the menu is
|
||||
# rebuilt from whatever is currently researched (#21) rather than being a
|
||||
# fixed arrangement with holes punched in it.
|
||||
const MENU_ROW_TOPS: Array = [-300.0, -10.0, 280.0]
|
||||
const MENU_ROW_TOPS: Array = [-300.0, -77.0, 146.0, 369.0]
|
||||
const MENU_COL_X: float = 140.0
|
||||
const MENU_BUTTON_HALF: float = 64.0
|
||||
const MENU_BUTTON_H: float = 128.0
|
||||
const MENU_BUTTON_H: float = 100.0
|
||||
const MENU_LABEL_HALF: float = 150.0
|
||||
const MENU_LABEL_H: float = 66.0
|
||||
const MENU_LABEL_H: float = 52.0
|
||||
const MENU_COST_HALF: float = 60.0
|
||||
const MENU_COST_H: float = 52.0
|
||||
const MENU_COST_H: float = 44.0
|
||||
# Gaps from each row's own top to where the label/cost start, sized to the
|
||||
# shrunk MENU_*_H values above (#54: fitting a 4th row meant tightening
|
||||
# MENU_ROW_TOPS' spacing to 223px, less than the original 256px a row's
|
||||
# button+label+cost needed - shrinking those closed the gap without
|
||||
# resorting to a scrollable menu).
|
||||
const MENU_LABEL_OFFSET: float = 106.0
|
||||
const MENU_COST_OFFSET: float = 162.0
|
||||
|
||||
|
||||
func _place_entry(node: Control, cx: float, top: float, half: float, height: float) -> void:
|
||||
|
|
@ -530,8 +537,8 @@ func _layout_build_menu() -> void:
|
|||
var cx: float = 0.0 if alone else (-MENU_COL_X if i % 2 == 0 else MENU_COL_X)
|
||||
var top: float = MENU_ROW_TOPS[mini(row, MENU_ROW_TOPS.size() - 1)]
|
||||
_place_entry(shown[i][1], cx, top, MENU_BUTTON_HALF, MENU_BUTTON_H)
|
||||
_place_entry(shown[i][2], cx, top + 134.0, MENU_LABEL_HALF, MENU_LABEL_H)
|
||||
_place_entry(shown[i][3], cx, top + 204.0, MENU_COST_HALF, MENU_COST_H)
|
||||
_place_entry(shown[i][2], cx, top + MENU_LABEL_OFFSET, MENU_LABEL_HALF, MENU_LABEL_H)
|
||||
_place_entry(shown[i][3], cx, top + MENU_COST_OFFSET, MENU_COST_HALF, MENU_COST_H)
|
||||
|
||||
|
||||
func _refresh_build_status() -> void:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
class_name BuildingDefinitions
|
||||
extends RefCounted
|
||||
|
||||
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "extractor", "transport_hub", "ship_lab"]
|
||||
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "extractor", "transport_hub", "ship_lab", "gas_burner"]
|
||||
const DEFINITIONS: Dictionary = {
|
||||
"mothership": preload("res://MothershipBuildingDefinition.tres"),
|
||||
"ship_crafter": preload("res://CrafterBuildingDefinition.tres"),
|
||||
|
|
@ -9,6 +9,7 @@ const DEFINITIONS: Dictionary = {
|
|||
"extractor": preload("res://ExtractorBuildingDefinition.tres"),
|
||||
"transport_hub": preload("res://TransportHubBuildingDefinition.tres"),
|
||||
"ship_lab": preload("res://ShipLabBuildingDefinition.tres"),
|
||||
"gas_burner": preload("res://GasBurnerBuildingDefinition.tres"),
|
||||
}
|
||||
|
||||
static func get_definition(definition_id: String) -> Resource:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ const CRAFTER_COLOR: Color = Color(1.0, 0.72, 0.25)
|
|||
const COMMAND_COLOR: Color = Color(0.55, 0.6, 1.0)
|
||||
const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85)
|
||||
const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9)
|
||||
const GAS_BURNER_COLOR: Color = Color(0.95, 0.35, 0.15)
|
||||
const GAS_BURNER_ENERGY_COLOR: Color = Color(0.85, 0.95, 0.15)
|
||||
|
||||
# Ship lab: a test frame with a vessel held in it and instruments sweeping
|
||||
# over it. Reads as a workshop rather than a factory - the crafter already
|
||||
|
|
@ -278,6 +280,42 @@ func _animate_transport_hub() -> void:
|
|||
_hub_traffic.rotation = phase_time * HUB_SPIN_SPEED
|
||||
|
||||
|
||||
# --- gas burner (#54) ------------------------------------------------------
|
||||
# A squat furnace rather than more scaffolding: a hexagonal combustion
|
||||
# chamber with a hot inner core, fed by intake vents drawn in gas's own
|
||||
# colour and exhausting through ports glowing in energy's colour - so the
|
||||
# structure itself reads as "gas in, energy out" without needing a label.
|
||||
# Drawn once and never redrawn, same as the command centre.
|
||||
|
||||
const BURNER_CHAMBER_RADIUS: float = 50.0
|
||||
const BURNER_CORE_RADIUS: float = 34.0
|
||||
const BURNER_VENT_COUNT: int = 4
|
||||
const BURNER_VENT_LENGTH: float = 26.0
|
||||
const BURNER_PORT_RADIUS: float = 6.0
|
||||
|
||||
|
||||
func _draw_gas_burner_static(target: CanvasItem) -> void:
|
||||
var c: Color = GAS_BURNER_COLOR
|
||||
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, BURNER_CHAMBER_RADIUS, 6, PI / 6.0), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, BURNER_CORE_RADIUS, 6, PI / 6.0), c, 0.5,
|
||||
NeonDraw.DEFAULT_CORE, 0.7)
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, BURNER_CORE_RADIUS * 0.5, 6, PI / 6.0),
|
||||
Color(1.0, 0.75, 0.35), 0.85)
|
||||
|
||||
for i in range(BURNER_VENT_COUNT):
|
||||
var a: float = (float(i) / BURNER_VENT_COUNT) * TAU + PI / 4.0
|
||||
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||
var tip: Vector2 = dir * (BURNER_CHAMBER_RADIUS + BURNER_VENT_LENGTH)
|
||||
# Intake vent in gas's own colour, so it is unmistakably the feed
|
||||
# rather than another structural brace.
|
||||
NeonDraw.draw_glowing_line(target, dir * BURNER_CHAMBER_RADIUS, tip,
|
||||
Color(0.85, 0.52, 1.0), 0.7, 0.34)
|
||||
# Output port glowing in energy's colour, at the same vent tip -
|
||||
# the building's own silhouette shows both ends of the conversion.
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(tip, BURNER_PORT_RADIUS, 5), GAS_BURNER_ENERGY_COLOR, 0.9)
|
||||
|
||||
|
||||
func _diamond(centre: Vector2, r: float) -> PackedVector2Array:
|
||||
return PackedVector2Array([
|
||||
centre + Vector2(0, -r), centre + Vector2(r, 0),
|
||||
|
|
|
|||
11
EnergyResourceDefinition.tres
Normal file
11
EnergyResourceDefinition.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_resource type="Resource" script_class="ResourceDefinition" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ResourceDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://EnergyResourceGlyph.tscn" id="2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
resource_id = "energy"
|
||||
display_name = "Energy"
|
||||
color = Color(0.85, 0.95, 0.15, 1)
|
||||
glyph_scene = ExtResource("2")
|
||||
26
EnergyResourceGlyph.gd
Normal file
26
EnergyResourceGlyph.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
extends Control
|
||||
|
||||
@export var colour: Color = Color(0.85, 0.95, 0.15)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var centre: Vector2 = size * 0.5
|
||||
var radius: float = minf(size.x, size.y) * 0.34
|
||||
# A cell outline (battery/reactor cell, same hexagon family as the ore
|
||||
# glyph's own outline) with a bolt struck through it, so energy reads as
|
||||
# "stored power" rather than being confused with ore's plain hexagon.
|
||||
var cell := PackedVector2Array()
|
||||
for i in range(6):
|
||||
var angle: float = TAU * float(i) / 6.0 - PI * 0.5
|
||||
cell.append(centre + Vector2(cos(angle), sin(angle)) * radius)
|
||||
NeonDraw.draw_glowing_polygon(self, cell, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035))
|
||||
|
||||
var bolt := PackedVector2Array([
|
||||
centre + Vector2(radius * 0.18, -radius * 0.75),
|
||||
centre + Vector2(-radius * 0.32, radius * 0.05),
|
||||
centre + Vector2(radius * 0.05, radius * 0.05),
|
||||
centre + Vector2(-radius * 0.18, radius * 0.75),
|
||||
centre + Vector2(radius * 0.32, -radius * 0.05),
|
||||
centre + Vector2(radius * 0.0, -radius * 0.05),
|
||||
])
|
||||
NeonDraw.draw_glowing_polygon(self, bolt, colour.lightened(0.35), 1.0, colour.darkened(0.6), maxf(0.4, radius * 0.03))
|
||||
13
EnergyResourceGlyph.tscn
Normal file
13
EnergyResourceGlyph.tscn
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://EnergyResourceGlyph.gd" id="1"]
|
||||
|
||||
[node name="GlyphVisual" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
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
|
||||
|
|
|
|||
20
GasBurnerBuildingDefinition.tres
Normal file
20
GasBurnerBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://GasBurnerBuildingVisual.tscn" id="2"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "gas_burner"
|
||||
display_name = "Gas Burner"
|
||||
description = "Burns gas into energy at its own site. Hub transporters keep it stocked with gas and collect the energy it makes."
|
||||
cost = 30
|
||||
action_icon = 33
|
||||
menu_icon = 33
|
||||
visual_scene = ExtResource("2")
|
||||
capabilities = PackedStringArray("gas_burner")
|
||||
map_color = Color(0.95, 0.35, 0.15, 1)
|
||||
map_sides = 5
|
||||
map_radius = 7.0
|
||||
map_phase = 0.0
|
||||
menu_button_name = "GasBurnerButton"
|
||||
menu_label_name = "GasBurnerLabel"
|
||||
menu_cost_name = "GasBurnerCost"
|
||||
9
GasBurnerBuildingVisual.gd
Normal file
9
GasBurnerBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
extends "res://BuildingVisualBase.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(false)
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
register_draw()
|
||||
_draw_gas_burner_static(self)
|
||||
11
GasBurnerBuildingVisual.tscn
Normal file
11
GasBurnerBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://GasBurnerBuildingVisual.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"]
|
||||
|
||||
[node name="Visual" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ActionProvider" type="Node" parent="."]
|
||||
script = ExtResource("2")
|
||||
actions = PackedStringArray("transfer")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
42
Main.tscn
42
Main.tscn
|
|
@ -576,6 +576,48 @@ script = ExtResource("19")
|
|||
amount = 35
|
||||
digit_height = 40.0
|
||||
|
||||
[node name="GasBurnerButton" type="Control" parent="UI/BuildMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -64
|
||||
offset_right = 64
|
||||
offset_top = 570
|
||||
offset_bottom = 698
|
||||
script = ExtResource("10")
|
||||
icon = 33
|
||||
glow_color = Color(0.95, 0.35, 0.15, 1)
|
||||
|
||||
[node name="GasBurnerLabel" type="Label" parent="UI/BuildMenu"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -150
|
||||
offset_right = 150
|
||||
offset_top = 704
|
||||
offset_bottom = 770
|
||||
theme_override_font_sizes/font_size = 32
|
||||
autowrap_mode = 2
|
||||
text = "Gas Burner"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="GasBurnerCost" type="Control" parent="UI/BuildMenu"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -60
|
||||
offset_right = 60
|
||||
offset_top = 774
|
||||
offset_bottom = 826
|
||||
script = ExtResource("19")
|
||||
amount = 30
|
||||
digit_height = 40.0
|
||||
|
||||
[node name="CloseButton" type="Control" parent="UI/BuildMenu"]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ extends RefCounted
|
|||
|
||||
const ORE_ID: String = "ore"
|
||||
const GAS_ID: String = "gas"
|
||||
const ORDER: PackedStringArray = [ORE_ID, GAS_ID]
|
||||
const ENERGY_ID: String = "energy"
|
||||
const ORDER: PackedStringArray = [ORE_ID, GAS_ID, ENERGY_ID]
|
||||
const DEFINITIONS: Dictionary = {
|
||||
ORE_ID: preload("res://OreResourceDefinition.tres"),
|
||||
GAS_ID: preload("res://GasResourceDefinition.tres"),
|
||||
ENERGY_ID: preload("res://EnergyResourceDefinition.tres"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
21
tests/_gas_burner_build_menu_preview.gd
Normal file
21
tests/_gas_burner_build_menu_preview.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test - #54's build menu screenshot. Everything researched,
|
||||
# including power_systems, so all 7 buildings show at once: the 7th (gas
|
||||
# burner) is the first to land beyond the menu's original 3-row/6-slot
|
||||
# layout, worth checking for fit even though the ticket calls
|
||||
# BuildController._layout_build_menu() already generic enough to need no
|
||||
# changes.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_gas_burner_build_menu_preview.tscn /tmp/menu.png 20 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.buildings.clear()
|
||||
GameState.extractors.clear()
|
||||
GameState.rocks = 500
|
||||
GameState.unlocked = ["shipyards", "logistics", "coordination", "freight", "vessel_science", "power_systems"]
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("UI/BuildMenu").visible = true
|
||||
main.get_node("BuildController")._refresh_build_status()
|
||||
6
tests/_gas_burner_build_menu_preview.tscn
Normal file
6
tests/_gas_burner_build_menu_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_gas_burner_build_menu_preview.gd" id="1"]
|
||||
|
||||
[node name="GasBurnerBuildMenuPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
31
tests/_gas_burner_transfer_preview.gd
Normal file
31
tests/_gas_burner_transfer_preview.gd
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test - #54's "transfer menu with three resources laid out
|
||||
# cleanly" screenshot. A gas burner holding all three resources at once
|
||||
# (ore incidentally, gas as its fuel, energy as its output) exercises the
|
||||
# transfer list at its widest.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_gas_burner_transfer_preview.tscn /tmp/m.png 20 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.buildings.clear()
|
||||
GameState.extractors.clear()
|
||||
GameState.transporters.clear()
|
||||
GameState.rocks = 180
|
||||
GameState.gas = 90
|
||||
GameState.fleet_cargo = 24
|
||||
GameState.fleet_gas_cargo = 12
|
||||
GameState.unlocked = ["shipyards", "logistics", "freight", "power_systems"]
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_GAS_BURNER, Vector2(400.0, 0.0))
|
||||
var burner_id: int = int(GameState.buildings[-1].id)
|
||||
GameState.deposit_resource_to_building(burner_id, GameState.RESOURCE_ORE, 14)
|
||||
GameState.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 22)
|
||||
GameState.deposit_resource_to_building(burner_id, GameState.RESOURCE_ENERGY, 36)
|
||||
var bc: Node = main.get_node("BuildController")
|
||||
bc.transfer_building_id = burner_id
|
||||
main.get_node("UI/TransferMenu").visible = true
|
||||
bc._refresh_transfer()
|
||||
bc.transfer_status.text = "Moved 25 ore into this building"
|
||||
6
tests/_gas_burner_transfer_preview.tscn
Normal file
6
tests/_gas_burner_transfer_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_gas_burner_transfer_preview.gd" id="1"]
|
||||
|
||||
[node name="GasBurnerTransferPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
163
tests/test_gas_burner.gd
Normal file
163
tests/test_gas_burner.gd
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_gas_burner.gd
|
||||
# Forgejo #54: gas burner building + the energy resource, stored/moved
|
||||
# exactly like ore and gas.
|
||||
#
|
||||
# Modeled on tests/test_extractor_income.gd's shape: pure GameState state,
|
||||
# no scene loaded, poking buildings/inventories directly and waiting out
|
||||
# real production/transporter timing with generous margins rather than
|
||||
# fixed frame counts (SceneTree physics can run ahead of _process(), trap 5
|
||||
# in the repo notes).
|
||||
#
|
||||
# Covers:
|
||||
# A. energy is fully generic - in resource_types(), has its own
|
||||
# ResourceDefinition/glyph/colour - with no per-resource code anywhere
|
||||
# (ResourceBar/ResourceTransferList already iterate resource_types())
|
||||
# B. a burner with local gas converts it to energy, into its OWN
|
||||
# inventory, at the configured rate; with no gas it stalls and never
|
||||
# touches the bank
|
||||
# C. the building is gated behind its own research node
|
||||
# D. a hub transporter both delivers gas to a burner and collects its
|
||||
# energy, following the existing supply/pickup dispatch paths
|
||||
|
||||
var game_state: Node
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var mothership_id: int = 0
|
||||
var hub_id: int = 0
|
||||
var burner_id: int = 0
|
||||
var load_limit: int = 0
|
||||
var energy_before_stall: int = 0
|
||||
|
||||
const MOTHERSHIP_AT := Vector2.ZERO
|
||||
const HUB_AT := Vector2(150.0, 0.0)
|
||||
const BURNER_AT := Vector2(400.0, 0.0)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.rocks = 0
|
||||
game_state.gas = 0
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.reset_progression()
|
||||
|
||||
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
|
||||
game_state.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 20)
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
# --- A. energy is fully generic -------------------------------
|
||||
if not game_state.resource_types().has(GameState.RESOURCE_ENERGY):
|
||||
return _fail("energy is not in resource_types() - the HUD/transfer list would never show it")
|
||||
var definition: Resource = game_state.resource_definition(GameState.RESOURCE_ENERGY)
|
||||
if definition == null or definition.glyph_scene == null:
|
||||
return _fail("energy has no ResourceDefinition or glyph_scene")
|
||||
var energy_color: Color = game_state.resource_color(GameState.RESOURCE_ENERGY)
|
||||
if energy_color == game_state.resource_color(GameState.RESOURCE_ORE) \
|
||||
or energy_color == game_state.resource_color(GameState.RESOURCE_GAS):
|
||||
return _fail("energy shares a colour with ore or gas")
|
||||
print("energy is generic: in resource_types(), own definition, own colour %s" % energy_color)
|
||||
|
||||
# --- B. produces energy from local gas at the configured rate -
|
||||
if elapsed < 2.5:
|
||||
return false
|
||||
var gas_left: int = game_state.building_resource(burner_id, GameState.RESOURCE_GAS)
|
||||
var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
|
||||
var gas_burned: int = 20 - gas_left
|
||||
if gas_burned < 2:
|
||||
return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, GameState.GAS_BURN_RATE])
|
||||
if energy_made != gas_burned * GameState.ENERGY_PER_GAS:
|
||||
return _fail("burner made %d energy for %d gas burned, expected %dx" % [
|
||||
energy_made, gas_burned, GameState.ENERGY_PER_GAS
|
||||
])
|
||||
if game_state.rocks != 0 or game_state.gas != 0:
|
||||
return _fail("conversion touched the bank (rocks=%d gas=%d) - it must only use local stock" % [
|
||||
game_state.rocks, game_state.gas
|
||||
])
|
||||
print("burner converted %d gas -> %d energy locally, bank untouched" % [gas_burned, energy_made])
|
||||
|
||||
# --- stall with no gas -----------------------------------------
|
||||
var burner_inventory: RefCounted = game_state.building_inventory(burner_id)
|
||||
burner_inventory.withdraw(GameState.RESOURCE_GAS, burner_inventory.amount(GameState.RESOURCE_GAS))
|
||||
energy_before_stall = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if elapsed < 1.5:
|
||||
return false
|
||||
var energy_now: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
|
||||
if energy_now != energy_before_stall:
|
||||
return _fail("burner made energy with no local gas (energy %d -> %d) - it must stall" % [
|
||||
energy_before_stall, energy_now
|
||||
])
|
||||
print("stalled with no gas: energy held at %d" % energy_now)
|
||||
|
||||
# --- C. gated behind research -----------------------------------
|
||||
if game_state.is_building_unlocked(GameState.BUILDING_GAS_BURNER):
|
||||
return _fail("gas_burner is buildable with no research at all")
|
||||
var gate: String = game_state.research_for_building(GameState.BUILDING_GAS_BURNER)
|
||||
if gate == "":
|
||||
return _fail("no research node gates gas_burner")
|
||||
game_state.unlocked = ["shipyards", "logistics", "freight", "power_systems"]
|
||||
if not game_state.is_building_unlocked(GameState.BUILDING_GAS_BURNER):
|
||||
return _fail("still locked after researching '%s'" % gate)
|
||||
print("locked behind research '%s', unlockable" % gate)
|
||||
|
||||
# --- D. logistics: reset for a fresh delivery/pickup test -----
|
||||
game_state.buildings.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.gas = 200
|
||||
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
hub_id = game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
||||
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
|
||||
if game_state.transporters.size() != game_state.hub_capacity():
|
||||
return _fail("hub did not provision its transporters")
|
||||
load_limit = game_state.transporter_load_limit()
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
# --- a transporter delivers gas to the burner ------------------
|
||||
if game_state.building_resource(burner_id, GameState.RESOURCE_GAS) > 0:
|
||||
print("transporter delivered gas to the burner: %d" % [
|
||||
game_state.building_resource(burner_id, GameState.RESOURCE_GAS)
|
||||
])
|
||||
# Force the burner's local energy up to a full load so pickup
|
||||
# dispatch fires immediately, rather than waiting out the
|
||||
# genuinely slow conversion rate for a second, separate check.
|
||||
game_state.building_inventory(burner_id).set_amount(GameState.RESOURCE_ENERGY, load_limit)
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
if elapsed > 10.0:
|
||||
return _fail("no gas was ever delivered to the burner")
|
||||
return false
|
||||
3:
|
||||
# --- a transporter collects the burner's energy to the bank ---
|
||||
if game_state.banked_resource(GameState.RESOURCE_ENERGY) >= load_limit:
|
||||
print("transporter collected %d energy to the bank" % game_state.banked_resource(GameState.RESOURCE_ENERGY))
|
||||
print("TEST_GAS_BURNER: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
if elapsed > 10.0:
|
||||
return _fail("burner's energy was never collected (bank=%d burner=%d)" % [
|
||||
game_state.banked_resource(GameState.RESOURCE_ENERGY),
|
||||
game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY),
|
||||
])
|
||||
return false
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_GAS_BURNER: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
Loading…
Reference in a new issue