Add manual extractor resource transfers

This commit is contained in:
Loïc Wisniewski 2026-08-14 22:21:01 +02:00
commit db0e6a0ec2
7 changed files with 180 additions and 14 deletions

View file

@ -159,9 +159,12 @@ var craft_message_until: int = 0
# current_building while any menu is open, so by the next frame the panel
# would not know which building it belongs to.
var transfer_building_id: int = 0
var transfer_extractor_index: int = -1
var current_building: Node2D = null
var pending_building: Node2D = null
var current_extractor_index: int = -1
var pending_extractor_index: int = -1
var pending_time: float = 0.0
var context_actions_expanded: bool = false
@ -304,6 +307,8 @@ func _process(delta: float) -> void:
or transfer_menu.visible or hangar_menu.visible:
current_building = null
pending_building = null
current_extractor_index = -1
pending_extractor_index = -1
action_button.visible = false
building_action_button.visible = false
transfer_button.visible = false
@ -314,17 +319,25 @@ func _process(delta: float) -> void:
# Debounced exactly like the enter-atmosphere prompt, so the action
# button can't flicker while the fleet drifts on the interact boundary.
var detected: Node2D = building_field.get_building_fleet_is_on()
var detected_extractor: int = -1
if detected == null:
var planet: Node2D = _planet_under_fleet()
if planet != null:
detected_extractor = GameState.extractor_index_at(planet.planet_id)
if detected != pending_building:
if detected != pending_building or detected_extractor != pending_extractor_index:
pending_building = detected
pending_extractor_index = detected_extractor
pending_time = 0.0
else:
pending_time += delta
if pending_time < prompt_debounce_time or pending_building == current_building:
if pending_time < prompt_debounce_time or (pending_building == current_building \
and pending_extractor_index == current_extractor_index):
return
current_building = pending_building
current_extractor_index = pending_extractor_index
context_actions_expanded = false
hangar_button.visible = false
# Every building can hold ore, so the transfer button rides alongside the
@ -332,13 +345,16 @@ func _process(delta: float) -> void:
# turns into - the two are independent choices at the same structure.
transfer_button.visible = false
building_action_button.visible = false
action_button.visible = current_building != null
action_button.visible = current_building != null or current_extractor_index >= 0
if current_building != null:
var actions: Array = _context_action_buttons()
building_action_button.icon = int(current_building.definition.action_icon)
building_action_button.queue_redraw()
action_button.icon = building_action_button.icon if actions.size() == 1 else IconButton.Icon.ACTION_MENU
action_button.queue_redraw()
elif current_extractor_index >= 0:
action_button.icon = IconButton.Icon.TRANSFER
action_button.queue_redraw()
_layout_context_buttons()
@ -365,6 +381,8 @@ func _layout_context_buttons() -> void:
func _context_action_buttons() -> Array:
if current_extractor_index >= 0:
return [transfer_button]
if current_building == null:
return []
var buttons: Array = []
@ -384,7 +402,7 @@ func _button_for_building_action(action: String) -> IconButton:
func _on_context_action_pressed() -> void:
if current_building == null:
if current_building == null and current_extractor_index < 0:
return
var actions: Array = _context_action_buttons()
if actions.size() == 1:
@ -877,9 +895,10 @@ const ALL: int = 1 << 30
func _on_transfer_pressed() -> void:
if current_building == null:
if current_building == null and current_extractor_index < 0:
return
transfer_building_id = current_building.building_id
transfer_building_id = current_building.building_id if current_building != null else 0
transfer_extractor_index = current_extractor_index
for bar in resource_bars:
bar.building_id = transfer_building_id
transfer_menu.visible = true
@ -892,15 +911,17 @@ func _on_transfer(to_building: bool, amount: int) -> void:
func _on_resource_transfer(resource_type: String, to_building: bool, amount: int = ALL) -> void:
if transfer_building_id == 0:
transfer_status.text = "No building selected - reopen this panel from the building"
if transfer_building_id == 0 and transfer_extractor_index < 0:
transfer_status.text = "No resource store selected - reopen this panel from its action"
return
var moved: int = GameState.transfer_resource_to_building(
transfer_building_id, resource_type, amount
) if to_building else GameState.transfer_resource_to_fleet(
transfer_building_id, resource_type, amount
)
var moved: int
if transfer_extractor_index >= 0:
moved = GameState.transfer_resource_to_extractor(transfer_extractor_index, resource_type, amount) \
if to_building else GameState.transfer_resource_from_extractor(transfer_extractor_index, resource_type, amount)
else:
moved = GameState.transfer_resource_to_building(transfer_building_id, resource_type, amount) \
if to_building else GameState.transfer_resource_to_fleet(transfer_building_id, resource_type, amount)
var resource_name: String = GameState.resource_name(resource_type).to_lower()
# Nothing moving is the common case at an empty end, and a silent no-op
@ -922,6 +943,8 @@ func _on_gas_transfer(to_building: bool) -> void:
func _transfer_place_name() -> String:
if transfer_extractor_index >= 0:
return "the extractor"
var b: Dictionary = GameState.find_building(transfer_building_id)
if b.is_empty():
return "this building"
@ -933,6 +956,9 @@ func _transfer_place_name() -> String:
func _refresh_transfer() -> void:
if transfer_extractor_index >= 0:
transfer_list.set_extractor_destination(transfer_extractor_index, "Planet extractor")
return
var building: Dictionary = GameState.find_building(transfer_building_id)
var heading: String = "This building"
if not building.is_empty() and building.type == GameState.BUILDING_MOTHERSHIP:

View file

@ -896,6 +896,14 @@ func has_extractor_at(planet_key: String) -> bool:
return false
func extractor_index_at(planet_key: String) -> int:
for i in range(extractors.size()):
var extractor: Dictionary = extractors[i]
if String(extractor.get("planet_id", extractor.get("planet_name", ""))) == planet_key:
return i
return -1
signal extractor_added(planet_name: String)
@ -924,6 +932,24 @@ func extractor_stock(index: int) -> int:
return _extractor_inventory(extractor).amount(String(extractor.get("resource_type", RESOURCE_ORE)))
func extractor_resource(index: int, resource_type: String) -> int:
if index < 0 or index >= extractors.size():
return 0
return _extractor_inventory(extractors[index]).amount(resource_type)
func transfer_resource_to_extractor(index: int, resource_type: String, amount: int) -> int:
if index < 0 or index >= extractors.size():
return 0
return fleet_inventory.transfer_to(_extractor_inventory(extractors[index]), resource_type, amount)
func transfer_resource_from_extractor(index: int, resource_type: String, amount: int) -> int:
if index < 0 or index >= extractors.size():
return 0
return _extractor_inventory(extractors[index]).transfer_to(fleet_inventory, resource_type, amount)
func _extractor_inventory(extractor: Dictionary) -> RefCounted:
var inventory: RefCounted = extractor.get("inventory") as RefCounted
if inventory == null:

View file

@ -15,6 +15,7 @@ const ALL_AMOUNT: int = 1 << 30
const AMOUNT_OPTIONS: Array[int] = [1, 5, 25, ALL_AMOUNT]
var building_id: int = 0
var extractor_index: int = -1
var _destination_heading: Label
var _rows: Dictionary = {}
@ -134,6 +135,15 @@ func _refresh_amount_controls(row: Dictionary) -> void:
func set_destination(id: int, heading: String) -> void:
building_id = id
extractor_index = -1
if _destination_heading != null:
_destination_heading.text = heading.to_upper()
refresh()
func set_extractor_destination(index: int, heading: String) -> void:
building_id = 0
extractor_index = index
if _destination_heading != null:
_destination_heading.text = heading.to_upper()
refresh()
@ -145,7 +155,8 @@ func refresh() -> void:
if row.is_empty():
continue
var fleet_amount: int = GameState.fleet_resource(resource_type)
var local_amount: int = GameState.building_resource(building_id, resource_type)
var local_amount: int = GameState.extractor_resource(extractor_index, resource_type) \
if extractor_index >= 0 else GameState.building_resource(building_id, resource_type)
row.fleet_amount.text = str(fleet_amount)
row.local_amount.text = str(local_amount)
row.unload.enabled = fleet_amount > 0

View file

@ -0,0 +1,24 @@
extends Node2D
func _ready() -> void:
GameState.extractors.clear()
GameState.transporters.clear()
GameState.fleet_cargo = 9
GameState.fleet_gas_cargo = 4
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)
var planet: Node2D
for planets in main.get_node("PlanetField").populated_chunks.values():
if not planets.is_empty() and is_instance_valid(planets[0]):
planet = planets[0]
break
if planet == null:
return
GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type, planet.planet_id)
var index: int = GameState.extractors.size() - 1
GameState.extractors[index].inventory.deposit(planet.resource_type, 38)
var controller: Node = main.get_node("BuildController")
controller.transfer_extractor_index = index
main.get_node("UI/TransferMenu").visible = true
controller._refresh_transfer()

View file

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_extractor_transfer_preview.gd" id="1"]
[node name="ExtractorTransferPreview" type="Node2D"]
script = ExtResource("1")

View file

@ -0,0 +1,74 @@
extends SceneTree
var state: Node
var scene: Node
var fleet: Node
var controller: Node
var planet: Node2D
var extractor_index: int = -1
var resource_type: String
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
state = root.get_node("GameState")
state.extractors.clear()
state.transporters.clear()
state.fleet_cargo = 0
state.fleet_gas_cargo = 0
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
controller = scene.get_node("BuildController")
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
for planets in scene.get_node("PlanetField").populated_chunks.values():
if not planets.is_empty() and is_instance_valid(planets[0]):
planet = planets[0]
break
if planet == null:
return _fail("no nearby generated planet available")
resource_type = planet.resource_type
state.add_extractor(planet.planet_name, planet.position, resource_type, planet.planet_id)
extractor_index = state.extractors.size() - 1
state.extractors[extractor_index].inventory.deposit(resource_type, 12)
for ship in fleet.ships:
ship.position = planet.position
fleet.move_to(planet.position)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 1.2:
return false
if controller.current_extractor_index != extractor_index or not controller.action_button.visible:
return _fail("extractor did not expose a contextual action")
if not scene.get_node("UI/EnterAtmosphereButton").visible:
return _fail("extractor action hid the separate landing action")
controller._on_context_action_pressed()
if not controller.transfer_menu.visible or controller.transfer_list.extractor_index != extractor_index:
return _fail("transfer panel did not bind to the extractor")
var before: int = state.fleet_resource(resource_type)
controller._on_resource_transfer(resource_type, false, 5)
if state.fleet_resource(resource_type) != before + 5:
return _fail("fleet did not fetch five resources from extractor")
if state.extractor_resource(extractor_index, resource_type) < 7:
return _fail("extractor transfer removed too much stock")
print("fleet fetched %s from extractor while landing remained available" % resource_type)
print("TEST_EXTRACTOR_TRANSFER: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_EXTRACTOR_TRANSFER: FAIL (%s)" % message)
quit(1)
return true

View file

@ -0,0 +1 @@
uid://0ghcdnlq2vd2