Refactor multi-resource UI and transfers

This commit is contained in:
Loïc Wisniewski 2026-08-14 11:54:31 +02:00
commit 8231e165ff
20 changed files with 648 additions and 435 deletions

View file

@ -125,25 +125,14 @@ var lab_building_id: int = 0
@onready var transfer_menu: Control = get_node("../UI/TransferMenu")
@onready var transfer_status: Label = get_node("../UI/TransferMenu/StatusLabel")
@onready var transfer_close_button: IconButton = get_node("../UI/TransferMenu/CloseButton")
@onready var to_building_label: Label = get_node("../UI/TransferMenu/ToBuildingLabel")
@onready var to_building_batch_button: IconButton = get_node("../UI/TransferMenu/ToBuildingBatchButton")
@onready var to_building_batch_label: Label = get_node("../UI/TransferMenu/ToBuildingBatchLabel")
@onready var to_building_all_label: Label = get_node("../UI/TransferMenu/ToBuildingAllLabel")
@onready var to_building_all_button: IconButton = get_node("../UI/TransferMenu/ToBuildingAllButton")
@onready var to_fleet_label: Label = get_node("../UI/TransferMenu/ToFleetLabel")
@onready var to_fleet_batch_button: IconButton = get_node("../UI/TransferMenu/ToFleetBatchButton")
@onready var to_fleet_batch_label: Label = get_node("../UI/TransferMenu/ToFleetBatchLabel")
@onready var to_fleet_all_label: Label = get_node("../UI/TransferMenu/ToFleetAllLabel")
@onready var to_fleet_all_button: IconButton = get_node("../UI/TransferMenu/ToFleetAllButton")
@onready var gas_to_building_button: IconButton = get_node("../UI/TransferMenu/GasToBuildingButton")
@onready var gas_to_fleet_button: IconButton = get_node("../UI/TransferMenu/GasToFleetButton")
@onready var transfer_list: ResourceTransferList = get_node("../UI/TransferMenu/ResourceList")
# Every building menu carries the same header (#24). Held as a list because
# the only thing any of them needs is to be told which building it is
# reporting on when its panel opens.
@onready var resource_bars: Array = [
get_node("../UI/CraftMenu/Bar"), get_node("../UI/SkillMenu/Bar"),
get_node("../UI/LabMenu/Bar"), get_node("../UI/TransferMenu/Bar"),
get_node("../UI/LabMenu/Bar"),
]
@onready var skill_menu: Control = get_node("../UI/SkillMenu")
@ -253,12 +242,7 @@ func _ready() -> void:
for i in range(queue_slots.size()):
queue_slots[i].pressed.connect(_on_queue_slot.bind(i))
transfer_close_button.pressed.connect(func(): transfer_menu.visible = false)
to_building_batch_button.pressed.connect(_on_transfer.bind(true, GameState.CARGO_LOAD_BATCH))
to_building_all_button.pressed.connect(_on_transfer.bind(true, ALL))
to_fleet_batch_button.pressed.connect(_on_transfer.bind(false, GameState.CARGO_LOAD_BATCH))
to_fleet_all_button.pressed.connect(_on_transfer.bind(false, ALL))
gas_to_building_button.pressed.connect(_on_gas_transfer.bind(true))
gas_to_fleet_button.pressed.connect(_on_gas_transfer.bind(false))
transfer_list.transfer_requested.connect(_on_resource_transfer)
formation_close_button.pressed.connect(func(): formation_menu.visible = false)
formation_reset_button.pressed.connect(_on_reset_formation)
@ -978,40 +962,45 @@ func _on_transfer_pressed() -> void:
if current_building == null:
return
transfer_building_id = current_building.building_id
for bar in resource_bars:
bar.building_id = transfer_building_id
transfer_menu.visible = true
transfer_status.text = ""
_refresh_transfer()
func _on_transfer(to_building: bool, amount: int) -> void:
_on_resource_transfer(GameState.RESOURCE_ORE, to_building, amount)
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"
return
var moved: int = 0
if to_building:
moved = GameState.transfer_to_building(transfer_building_id, amount)
else:
moved = GameState.transfer_to_fleet(transfer_building_id, amount)
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 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
# is the dead-button problem this panel exists to remove. Say which side
# was empty rather than just refusing.
if moved <= 0:
if to_building:
transfer_status.text = "The fleet is carrying nothing to unload"
transfer_status.text = "The fleet is carrying nothing to unload (no %s)" % resource_name
else:
transfer_status.text = "%s has no ore to load" % _transfer_place_name()
transfer_status.text = "%s has no %s to load" % [_transfer_place_name().capitalize(), resource_name]
elif to_building:
transfer_status.text = "Moved %d ore into %s" % [moved, _transfer_place_name()]
transfer_status.text = "Moved %d %s into %s" % [moved, resource_name, _transfer_place_name()]
else:
transfer_status.text = "Loaded %d ore into the fleet" % moved
transfer_status.text = "Loaded %d %s into the fleet" % [moved, resource_name]
_refresh_transfer()
func _on_gas_transfer(to_building: bool) -> void:
var moved: int = GameState.transfer_gas_to_building(transfer_building_id, ALL) if to_building else GameState.transfer_gas_to_fleet(transfer_building_id, ALL)
transfer_status.text = "Moved %d gas" % moved if moved > 0 else "No gas available on that side"
_refresh_transfer()
_on_resource_transfer(GameState.RESOURCE_GAS, to_building)
func _transfer_place_name() -> String:
@ -1026,38 +1015,11 @@ func _transfer_place_name() -> String:
func _refresh_transfer() -> void:
for bar in resource_bars:
bar.building_id = transfer_building_id
var hold: int = GameState.fleet_cargo
var here: int = GameState.building_holdings(transfer_building_id)
var batch: int = GameState.CARGO_LOAD_BATCH
to_building_label.text = "ORE"
to_fleet_label.text = "GAS"
to_building_batch_button.visible = false
to_building_batch_label.visible = false
to_fleet_batch_button.visible = false
to_fleet_batch_label.visible = false
to_building_all_label.text = "Unload all"
to_fleet_all_label.text = "Load all"
# The batch buttons say what they would actually move, not the nominal
# batch size: offering "25" when the hold has 4 in it is a promise the
# press cannot keep.
to_building_batch_label.text = "%d" % mini(batch, hold)
to_fleet_batch_label.text = "%d" % mini(batch, here)
to_building_batch_button.enabled = hold > 0
to_building_all_button.enabled = hold > 0
to_fleet_batch_button.enabled = here > 0
to_fleet_all_button.enabled = here > 0
for button in [to_building_batch_button, to_building_all_button,
to_fleet_batch_button, to_fleet_all_button]:
button.queue_redraw()
gas_to_building_button.enabled = GameState.fleet_gas_cargo > 0
gas_to_fleet_button.enabled = GameState.building_gas_holdings(transfer_building_id) > 0
gas_to_building_button.queue_redraw()
gas_to_fleet_button.queue_redraw()
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:
heading = "Mothership bank"
transfer_list.set_destination(transfer_building_id, heading)
func _refresh_queue() -> void:

View file

@ -44,6 +44,22 @@ var gas: int = 0
var fleet_cargo: int = 0
var fleet_gas_cargo: int = 0
# Resource-facing UI and logistics iterate this list instead of growing a
# second set of controls and controller branches every time a material is
# introduced. Keep presentation data here with the state it describes so a
# resource has one name and colour everywhere it appears.
const RESOURCE_ORE: String = "ore"
const RESOURCE_GAS: String = "gas"
const RESOURCE_TYPES: PackedStringArray = [RESOURCE_ORE, RESOURCE_GAS]
const RESOURCE_NAMES: Dictionary = {
RESOURCE_ORE: "Ore",
RESOURCE_GAS: "Gas",
}
const RESOURCE_COLORS: Dictionary = {
RESOURCE_ORE: Color(0.0, 0.72, 0.82),
RESOURCE_GAS: Color(0.85, 0.52, 1.0),
}
func collect_cargo(amount: int = 1) -> void:
fleet_cargo += amount
@ -51,6 +67,83 @@ func collect_gas_cargo(amount: int = 1) -> void:
fleet_gas_cargo += amount
func resource_name(resource_type: String) -> String:
return String(RESOURCE_NAMES.get(resource_type, resource_type.capitalize()))
func resource_color(resource_type: String) -> Color:
return RESOURCE_COLORS.get(resource_type, Color.WHITE)
func banked_resource(resource_type: String) -> int:
match resource_type:
RESOURCE_ORE: return rocks
RESOURCE_GAS: return gas
_: return 0
func fleet_resource(resource_type: String) -> int:
match resource_type:
RESOURCE_ORE: return fleet_cargo
RESOURCE_GAS: return fleet_gas_cargo
_: return 0
func building_resource(id: int, resource_type: String) -> int:
var building: Dictionary = find_building(id)
if building.is_empty():
return 0
if building.type == BUILDING_MOTHERSHIP:
return banked_resource(resource_type)
match resource_type:
RESOURCE_ORE: return int(building.get("stock", 0))
RESOURCE_GAS: return int(building.get("gas_stock", 0))
_: return 0
func transfer_resource_to_building(id: int, resource_type: String, amount: int) -> int:
var building: Dictionary = find_building(id)
if building.is_empty():
return 0
var moved: int = mini(maxi(amount, 0), fleet_resource(resource_type))
if moved <= 0:
return 0
match resource_type:
RESOURCE_ORE: fleet_cargo -= moved
RESOURCE_GAS: fleet_gas_cargo -= moved
_: return 0
if building.type == BUILDING_MOTHERSHIP:
match resource_type:
RESOURCE_ORE: rocks += moved
RESOURCE_GAS: gas += moved
else:
match resource_type:
RESOURCE_ORE: building.stock = int(building.get("stock", 0)) + moved
RESOURCE_GAS: building.gas_stock = int(building.get("gas_stock", 0)) + moved
return moved
func transfer_resource_to_fleet(id: int, resource_type: String, amount: int) -> int:
var building: Dictionary = find_building(id)
if building.is_empty():
return 0
var moved: int = mini(maxi(amount, 0), building_resource(id, resource_type))
if moved <= 0:
return 0
if building.type == BUILDING_MOTHERSHIP:
match resource_type:
RESOURCE_ORE: rocks -= moved
RESOURCE_GAS: gas -= moved
else:
match resource_type:
RESOURCE_ORE: building.stock = int(building.get("stock", 0)) - moved
RESOURCE_GAS: building.gas_stock = int(building.get("gas_stock", 0)) - moved
match resource_type:
RESOURCE_ORE: fleet_cargo += moved
RESOURCE_GAS: fleet_gas_cargo += moved
return moved
# The batch size the transfer menu offers, next to "All". A batch rather
# than only all-or-nothing: the hold is the part that can be lost, and
# emptying the bank in one tap would strand research, which spends from it.
@ -70,70 +163,29 @@ const CARGO_LOAD_BATCH: int = 25
func building_holdings(id: int) -> int:
var b: Dictionary = find_building(id)
if b.is_empty():
return 0
if b.type == BUILDING_MOTHERSHIP:
return rocks
return int(b.get("stock", 0))
return building_resource(id, RESOURCE_ORE)
func building_gas_holdings(id: int) -> int:
var b: Dictionary = find_building(id)
if b.is_empty(): return 0
return gas if b.type == BUILDING_MOTHERSHIP else int(b.get("gas_stock", 0))
return building_resource(id, RESOURCE_GAS)
func transfer_to_building(id: int, amount: int) -> int:
# Hold -> this building. Returns how much actually moved, which is capped
# by the hold: a caller asking for "all" passes a number too big on
# purpose and reads the result back.
var b: Dictionary = find_building(id)
if b.is_empty():
return 0
var moved: int = mini(amount, fleet_cargo)
if moved <= 0:
return 0
fleet_cargo -= moved
if b.type == BUILDING_MOTHERSHIP:
rocks += moved
else:
b.stock = int(b.get("stock", 0)) + moved
return moved
return transfer_resource_to_building(id, RESOURCE_ORE, amount)
func transfer_to_fleet(id: int, amount: int) -> int:
# This building -> hold. Capped by what the building actually holds, so
# there is no overdraft in either direction.
var b: Dictionary = find_building(id)
if b.is_empty():
return 0
var moved: int = mini(amount, building_holdings(id))
if moved <= 0:
return 0
if b.type == BUILDING_MOTHERSHIP:
rocks -= moved
else:
b.stock = int(b.get("stock", 0)) - moved
fleet_cargo += moved
return moved
return transfer_resource_to_fleet(id, RESOURCE_ORE, amount)
func transfer_gas_to_building(id: int, amount: int) -> int:
var b: Dictionary = find_building(id)
if b.is_empty(): return 0
var moved := mini(amount, fleet_gas_cargo)
fleet_gas_cargo -= moved
if b.type == BUILDING_MOTHERSHIP: gas += moved
else: b.gas_stock = int(b.get("gas_stock", 0)) + moved
return moved
return transfer_resource_to_building(id, RESOURCE_GAS, amount)
func transfer_gas_to_fleet(id: int, amount: int) -> int:
var b: Dictionary = find_building(id)
if b.is_empty(): return 0
var moved := mini(amount, building_gas_holdings(id))
if b.type == BUILDING_MOTHERSHIP: gas -= moved
else: b.gas_stock = int(b.get("gas_stock", 0)) - moved
fleet_gas_cargo += moved
return moved
return transfer_resource_to_fleet(id, RESOURCE_GAS, amount)
const MOTHERSHIP_COST: int = 20
const SHIP_CRAFTER_COST: int = 12
@ -348,7 +400,8 @@ func add_building(type: String, at_position: Vector2) -> int:
var id: int = next_building_id
next_building_id += 1
buildings.append({
"id": id, "type": type, "position": at_position, "stock": 0, "orders": [],
"id": id, "type": type, "position": at_position, "stock": 0,
"gas_stock": 0, "orders": [],
})
return id

10
GasCargoCounter.gd Normal file
View file

@ -0,0 +1,10 @@
extends "res://GasCounter.gd"
# Fleet-side companion to GasCounter. It keeps the gas bubble glyph instead
# of recolouring ore, and occupies the same row as banked gas in the HUD.
func _process(_delta: float) -> void:
visible = GameState.fleet_gas_cargo > 0
if visible:
set_amount(GameState.fleet_gas_cargo)

View file

@ -18,4 +18,20 @@ func _process(delta: float) -> void:
progress = 0.0
func _draw() -> void:
for vent in vents:
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([vent+Vector2(0,-44),vent+Vector2(38,-22),vent+Vector2(38,22),vent+Vector2(0,44),vent+Vector2(-38,22),vent+Vector2(-38,-22)]), Color(0.85,0.52,1),0.8,Color(0.15,0.03,0.22),0.5)
_draw_gas_source(vent)
func _draw_gas_source(centre: Vector2) -> void:
var colour := Color(0.85, 0.52, 1.0)
var core := Color(0.15, 0.03, 0.22)
var bubbles: Array = [
[centre + Vector2(-20.0, 8.0), 20.0],
[centre + Vector2(18.0, 14.0), 27.0],
[centre + Vector2(17.0, -22.0), 15.0],
]
for bubble in bubbles:
var ring := PackedVector2Array()
for i in range(12):
var angle: float = TAU * float(i) / 12.0
ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1]))
NeonDraw.draw_glowing_polygon(self, ring, colour, 0.9, core, 0.6)

View file

@ -27,6 +27,7 @@ enum Icon {
UPGRADE_ENGINES, UPGRADE_UNLOCK_SHIELD,
ACTION_MENU,
GAS,
LEFT_ARROW, RIGHT_ARROW,
}
# Size the glyphs were originally drawn against. Every glyph dimension is
@ -203,6 +204,10 @@ func _draw() -> void:
_draw_action_menu(center, pulse, s)
Icon.GAS:
_draw_gas(center, pulse, s)
Icon.LEFT_ARROW:
_draw_horizontal_arrow(center, pulse, -1.0, s)
Icon.RIGHT_ARROW:
_draw_horizontal_arrow(center, pulse, 1.0, s)
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
@ -228,6 +233,15 @@ func _draw_arrow(center: Vector2, pulse: float, direction: float, s: float) -> v
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(r * 0.7, -r * 0.8 * direction), _active_color(), pulse, 0.5 * s, core_color)
func _draw_horizontal_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void:
var r: float = 10.0 * s
var tip: Vector2 = center + Vector2(r * direction, 0.0)
var tail: Vector2 = center + Vector2(-r * direction, 0.0)
NeonDraw.draw_glowing_line(self, tail, tip, _active_color(), pulse, 0.5 * s, core_color)
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.8 * direction, -r * 0.7), _active_color(), pulse, 0.5 * s, core_color)
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.8 * direction, r * 0.7), _active_color(), pulse, 0.5 * s, core_color)
func _draw_transfer_icon(center: Vector2, pulse: float, s: float) -> void:
# Two shafts side by side pointing opposite ways. Drawn as a pair rather
# than as one double-headed arrow because the button's whole job is that

249
Main.tscn
View file

@ -1,4 +1,4 @@
[gd_scene load_steps=23 format=3]
[gd_scene load_steps=29 format=3]
[ext_resource type="Script" path="res://Starfield.gd" id="1"]
[ext_resource type="Script" path="res://Fleet.gd" id="2"]
@ -25,6 +25,8 @@
[ext_resource type="Script" path="res://ResourceBar.gd" id="24"]
[ext_resource type="Script" path="res://GasCounter.gd" id="25"]
[ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"]
[ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"]
[ext_resource type="Script" path="res://GasCargoCounter.gd" id="28"]
[sub_resource type="ShaderMaterial" id="1"]
shader = ExtResource("5")
@ -90,48 +92,58 @@ layer = 1
[node name="ResourceCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 28
offset_top = 24
offset_top = 38
offset_right = 320
offset_bottom = 92
offset_bottom = 82
script = ExtResource("16")
digit_height = 60.0
digit_height = 38.0
[node name="OreCaption" type="Label" parent="UI"]
mouse_filter = 2
offset_left = 28
offset_top = 22
offset_right = 120
offset_bottom = 48
offset_top = 6
offset_right = 220
offset_bottom = 34
theme_override_font_sizes/font_size = 18
text = "ORE"
theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1)
text = "BANKED"
[node name="GasCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 294
offset_top = 24
offset_right = 468
offset_bottom = 92
script = ExtResource("25")
digit_height = 60.0
[node name="GasCaption" type="Label" parent="UI"]
mouse_filter = 2
offset_left = 294
offset_top = 22
offset_right = 380
offset_bottom = 48
theme_override_font_sizes/font_size = 18
theme_override_colors/font_color = Color(0.85, 0.52, 1, 1)
text = "GAS"
[node name="CargoCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 28
offset_top = 96
offset_right = 320
offset_bottom = 146
offset_bottom = 140
script = ExtResource("25")
digit_height = 38.0
[node name="GasCaption" type="Label" parent="UI"]
mouse_filter = 2
offset_left = 356
offset_top = 6
offset_right = 620
offset_bottom = 34
theme_override_font_sizes/font_size = 18
theme_override_colors/font_color = Color(1, 0.78, 0.25, 1)
text = "FLEET HOLD"
[node name="CargoCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 356
offset_top = 38
offset_right = 696
offset_bottom = 82
script = ExtResource("22")
digit_height = 42.0
digit_height = 38.0
[node name="GasCargoCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 356
offset_top = 96
offset_right = 696
offset_bottom = 140
script = ExtResource("28")
digit_height = 38.0
[node name="PlanetNameLabel" type="Label" parent="UI"]
visible = false
@ -947,10 +959,11 @@ horizontal_alignment = 1
[node name="Bar" type="Control" parent="UI/LabMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 180
offset_right = 520
offset_bottom = 260
offset_right = -24
offset_bottom = 264
script = ExtResource("24")
[node name="VesselA" type="Control" parent="UI/LabMenu"]
@ -1004,7 +1017,7 @@ anchor_left = 0.5
anchor_right = 0.5
offset_left = -330
offset_right = -130
offset_top = 376
offset_top = 400
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
@ -1258,7 +1271,7 @@ mouse_filter = 2
offset_left = 24
offset_top = 180
offset_right = 520
offset_bottom = 260
offset_bottom = 264
script = ExtResource("24")
[node name="StatusLabel" type="Label" parent="UI/CraftMenu"]
@ -1467,142 +1480,29 @@ offset_left = 24
offset_top = 36
offset_right = -200
offset_bottom = 166
text = "Move ore"
text = "Transfer cargo"
horizontal_alignment = 1
[node name="Bar" type="Control" parent="UI/TransferMenu"]
mouse_filter = 2
offset_left = 24
offset_top = 180
offset_right = 520
offset_bottom = 260
script = ExtResource("24")
[node name="StatusLabel" type="Label" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 274
offset_top = 184
offset_right = -24
offset_bottom = 396
theme_override_font_sizes/font_size = 34
offset_bottom = 284
theme_override_font_sizes/font_size = 30
autowrap_mode = 2
text = ""
horizontal_alignment = 1
[node name="ToBuildingLabel" type="Label" parent="UI/TransferMenu"]
[node name="ResourceList" type="Control" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 376
offset_top = 294
offset_right = -24
offset_bottom = 426
theme_override_font_sizes/font_size = 28
text = ""
horizontal_alignment = 1
[node name="ToBuildingBatchButton" type="Control" parent="UI/TransferMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -184
offset_right = -36
offset_top = 438
offset_bottom = 586
script = ExtResource("10")
icon = 3
glow_color = Color(0.45, 0.9, 0.55, 1)
[node name="ToBuildingBatchLabel" type="Label" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -230
offset_right = 10
offset_top = 592
offset_bottom = 638
theme_override_font_sizes/font_size = 24
text = ""
horizontal_alignment = 1
[node name="ToBuildingAllButton" type="Control" parent="UI/TransferMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -180
offset_right = -32
offset_top = 438
offset_bottom = 586
script = ExtResource("10")
icon = 3
glow_color = Color(0.45, 0.9, 0.55, 1)
[node name="ToBuildingAllLabel" type="Label" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -230
offset_right = 10
offset_top = 592
offset_bottom = 638
theme_override_font_sizes/font_size = 24
text = "Unload all"
horizontal_alignment = 1
[node name="ToFleetLabel" type="Label" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 680
offset_right = -24
offset_bottom = 730
theme_override_font_sizes/font_size = 28
text = ""
horizontal_alignment = 1
[node name="ToFleetBatchButton" type="Control" parent="UI/TransferMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -184
offset_right = -36
offset_top = 756
offset_bottom = 904
script = ExtResource("10")
icon = 4
glow_color = Color(1, 0.78, 0.25, 1)
[node name="ToFleetBatchLabel" type="Label" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -230
offset_right = 10
offset_top = 910
offset_bottom = 962
theme_override_font_sizes/font_size = 30
text = ""
horizontal_alignment = 1
[node name="ToFleetAllButton" type="Control" parent="UI/TransferMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 32
offset_right = 180
offset_top = 438
offset_bottom = 586
script = ExtResource("10")
icon = 4
glow_color = Color(1, 0.78, 0.25, 1)
[node name="ToFleetAllLabel" type="Label" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 10
offset_right = 230
offset_top = 592
offset_bottom = 638
theme_override_font_sizes/font_size = 24
text = "Load all"
horizontal_alignment = 1
offset_bottom = 1080
script = ExtResource("27")
[node name="CloseButton" type="Control" parent="UI/TransferMenu"]
anchor_left = 1.0
@ -1615,37 +1515,17 @@ script = ExtResource("10")
icon = 1
glow_color = Color(1, 0.78, 0.25, 1)
[node name="GasToBuildingButton" type="Control" parent="UI/TransferMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -180
offset_right = -32
offset_top = 742
offset_bottom = 890
script = ExtResource("10")
icon = 25
glow_color = Color(0.85, 0.52, 1, 1)
[node name="GasToFleetButton" type="Control" parent="UI/TransferMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 32
offset_right = 180
offset_top = 742
offset_bottom = 890
script = ExtResource("10")
icon = 25
glow_color = Color(0.85, 0.52, 1, 1)
[node name="GasTransferLabel" type="Label" parent="UI/TransferMenu"]
[node name="HintLabel" type="Label" parent="UI/TransferMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_top = 896
offset_bottom = 942
text = "Unload all Load all"
offset_left = 24
offset_top = 1092
offset_right = -24
offset_bottom = 1150
text = "Arrows move everything available on that side"
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.85, 0.52, 1, 1)
theme_override_font_sizes/font_size = 22
theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1)
theme_override_font_sizes/font_size = 20
[node name="FormationMenu" type="Control" parent="UI" groups=["modal_menus"]]
visible = false
@ -1755,10 +1635,11 @@ horizontal_alignment = 1
[node name="Bar" type="Control" parent="UI/SkillMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 180.0
offset_right = 520.0
offset_bottom = 260.0
offset_right = -24.0
offset_bottom = 264.0
script = ExtResource("24")
[node name="Tree" type="Control" parent="UI/SkillMenu"]

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=12 format=3]
[gd_scene load_steps=15 format=3]
[ext_resource type="Script" path="res://GameController.gd" id="1"]
[ext_resource type="Script" path="res://LunarBackground.gd" id="2"]
@ -10,6 +10,9 @@
[ext_resource type="Script" path="res://RockField.gd" id="8"]
[ext_resource type="Script" path="res://ResourceCounter.gd" id="9"]
[ext_resource type="Script" path="res://GasVentField.gd" id="10"]
[ext_resource type="Script" path="res://GasCounter.gd" id="11"]
[ext_resource type="Script" path="res://CargoCounter.gd" id="12"]
[ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"]
[sub_resource type="ShaderMaterial" id="1"]
shader = ExtResource("3")
@ -62,14 +65,65 @@ layer = 1
[node name="ResourceCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 28
offset_top = 24
offset_top = 38
offset_right = 320
offset_bottom = 92
offset_bottom = 82
script = ExtResource("9")
digit_height = 60.0
digit_height = 38.0
glow_color = Color(0, 0.42, 0.48, 1)
core_color = Color(0, 0.08, 0.1, 1)
[node name="BankCaption" type="Label" parent="UI"]
mouse_filter = 2
offset_left = 28
offset_top = 6
offset_right = 220
offset_bottom = 34
theme_override_font_sizes/font_size = 18
theme_override_colors/font_color = Color(0.08, 0.16, 0.2, 1)
theme_override_colors/font_outline_color = Color(0.9, 0.95, 0.96, 0.8)
theme_override_constants/outline_size = 4
text = "BANKED"
[node name="GasCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 28
offset_top = 96
offset_right = 320
offset_bottom = 140
script = ExtResource("11")
digit_height = 38.0
[node name="FleetCaption" type="Label" parent="UI"]
mouse_filter = 2
offset_left = 356
offset_top = 6
offset_right = 620
offset_bottom = 34
theme_override_font_sizes/font_size = 18
theme_override_colors/font_color = Color(0.24, 0.14, 0.02, 1)
theme_override_colors/font_outline_color = Color(0.96, 0.93, 0.82, 0.85)
theme_override_constants/outline_size = 4
text = "FLEET HOLD"
[node name="CargoCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 356
offset_top = 38
offset_right = 696
offset_bottom = 82
script = ExtResource("12")
digit_height = 38.0
[node name="GasCargoCounter" type="Control" parent="UI"]
mouse_filter = 2
offset_left = 356
offset_top = 96
offset_right = 696
offset_bottom = 140
script = ExtResource("13")
digit_height = 38.0
[node name="PlanetNameLabel" type="Label" parent="UI"]
offset_left = 24
offset_top = 156

View file

@ -20,10 +20,22 @@ var transitioning: bool = false
func _ready() -> void:
if GameState.planet_type == "gas_giant":
# A gas giant is not lunar terrain: its surface view uses a deep,
# coloured atmosphere instead of the rocky field.
# coloured atmosphere instead of the rocky field. The background is
# shader-driven, so changing ColorRect.color does not tint it; update
# the actual palette uniforms.
get_node("../RockField").visible = false
get_node("../GasVentField").visible = true
get_node("../BackgroundLayer/LunarBackground").color = Color(0.16, 0.035, 0.26, 1.0)
var background: ColorRect = get_node("../BackgroundLayer/LunarBackground")
var atmosphere: ShaderMaterial = background.material
atmosphere.set_shader_parameter("base_color", Color(0.13, 0.025, 0.22, 1.0))
atmosphere.set_shader_parameter("crater_color", Color(0.055, 0.008, 0.11, 1.0))
atmosphere.set_shader_parameter("highlight_color", Color(0.52, 0.17, 0.7, 1.0))
# The lunar surface uses dark captions on a bright ground. Flip only
# those captions for the night-dark gas atmosphere.
for caption_path in ["../UI/BankCaption", "../UI/FleetCaption"]:
var caption: Label = get_node(caption_path)
caption.add_theme_color_override("font_color", Color(0.78, 0.88, 0.94))
caption.add_theme_color_override("font_outline_color", Color(0.02, 0.01, 0.05, 0.9))
fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO)
camera.position = Vector2.ZERO
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)

View file

@ -1,146 +1,113 @@
class_name ResourceBar
extends Control
# The header every building menu now carries (#24): banked ore, what the
# fleet is holding, and what THIS building has. Before this, each panel
# improvised its own - the crafter spelled the same three figures into a
# run-on StatusLabel string, the research screen printed a bank total inside
# a button caption - so the same numbers looked different in every room.
#
# Three columns rather than a sentence, because the point of the bar is that
# a figure stays in the same place as the player moves between menus.
#
# Reads GameState directly every frame, like ResourceCounter and CargoCounter
# do, so it stays correct across the space <-> planet-surface scene swap with
# nobody having to push updates into it. Only `building_id` is pushed in,
# because "here" is the one thing the bar cannot work out for itself.
# Compact resource table shared by crafting, research and the ship lab.
# Rows are resources; columns are locations. Adding another material to
# GameState.RESOURCE_TYPES automatically gives it a row here.
# Which building the HERE column is reporting. 0 means none, which greys the
# column rather than showing a confident zero.
var building_id: int = 0
# The caption box has to be a full line tall, not just the font size: a Label
# renders its line height, not its point size, and the first version sat the
# digits 26px down from the top straight through the middle of the caption.
const CAPTION_HEIGHT: float = 34.0
const CAPTION_GAP: float = 4.0
const CAPTION_FONT_SIZE: int = 24
const DIGIT_HEIGHT: float = 34.0
# Bank keeps NeonNumber's own ore-teal, and the hold keeps the amber
# CargoCounter established for ore in transit. Only HERE needed a new colour;
# it borrows the mothership's green, which already reads as "landed
# somewhere and staying there" everywhere else in the game.
const BANK_GLOW: Color = Color(0.0, 0.62, 0.7)
const BANK_CORE: Color = Color(0.0, 0.12, 0.15)
const HOLD_GLOW: Color = Color(1.0, 0.78, 0.25)
const HOLD_CORE: Color = Color(0.22, 0.12, 0.0)
const HERE_GLOW: Color = Color(0.45, 0.9, 0.55)
const HERE_CORE: Color = Color(0.05, 0.16, 0.08)
const HEADER_HEIGHT: float = 22.0
const RULE_COLOR: Color = Color(0.25, 0.55, 0.7)
const FLEET_COLOR: Color = Color(1.0, 0.78, 0.25)
const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55)
var _numbers: Array[NeonNumber] = []
var _captions: Array[Label] = []
var _gas_caption: Label
var _gas_icon: IconButton
var _summary: Label
var _header_labels: Array[Label] = []
var _rows: Dictionary = {}
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
var columns: Array = [
["BANK", BANK_GLOW, BANK_CORE],
["HOLD", HOLD_GLOW, HOLD_CORE],
["HERE", HERE_GLOW, HERE_CORE],
]
for column in columns:
var caption: Label = Label.new()
caption.text = column[0]
caption.mouse_filter = Control.MOUSE_FILTER_IGNORE
caption.add_theme_font_size_override("font_size", CAPTION_FONT_SIZE)
caption.add_theme_color_override("font_color", column[1])
add_child(caption)
_captions.append(caption)
for heading in ["", "BANKED", "FLEET", "LOCAL"]:
var label := _make_label(heading, 16, Color(0.62, 0.78, 0.86))
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(label)
_header_labels.append(label)
var number: NeonNumber = NeonNumber.new()
number.mouse_filter = Control.MOUSE_FILTER_IGNORE
number.glow_color = column[1]
number.core_color = column[2]
number.digit_height = DIGIT_HEIGHT
add_child(number)
_numbers.append(number)
for resource_type in GameState.RESOURCE_TYPES:
var glyph := ResourceGlyph.new()
glyph.resource_type = resource_type
add_child(glyph)
var name_label := _make_label(GameState.resource_name(resource_type).to_upper(), 18,
GameState.resource_color(resource_type))
add_child(name_label)
var bank_label := _make_value_label(GameState.resource_color(resource_type))
var fleet_label := _make_value_label(FLEET_COLOR)
var local_label := _make_value_label(LOCAL_COLOR)
for label in [bank_label, fleet_label, local_label]:
add_child(label)
_rows[resource_type] = {
"glyph": glyph, "name": name_label, "bank": bank_label,
"fleet": fleet_label, "local": local_label,
}
_layout()
resized.connect(_layout)
_gas_caption = Label.new()
_gas_caption.mouse_filter = Control.MOUSE_FILTER_IGNORE
_gas_caption.add_theme_font_size_override("font_size", 20)
_gas_caption.add_theme_color_override("font_color", Color(0.85, 0.52, 1.0))
add_child(_gas_caption)
_gas_icon = IconButton.new()
_gas_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
_gas_icon.icon = IconButton.Icon.GAS
_gas_icon.glow_color = Color(0.85, 0.52, 1.0)
add_child(_gas_icon)
_summary = Label.new()
_summary.mouse_filter = Control.MOUSE_FILTER_IGNORE
_summary.add_theme_font_size_override("font_size", 18)
_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_summary.size = size
add_child(_summary)
_layout()
queue_redraw()
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
var label := Label.new()
label.text = text_value
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", colour)
label.add_theme_color_override("font_outline_color", Color(0.01, 0.01, 0.035, 0.95))
label.add_theme_constant_override("outline_size", 2)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
return label
func _make_value_label(colour: Color) -> Label:
var label := _make_label("0", 19, colour)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
return label
func _layout() -> void:
var column_width: float = size.x / float(_numbers.size())
for i in _numbers.size():
var x: float = column_width * float(i)
_captions[i].position = Vector2(x, 0.0)
_captions[i].size = Vector2(column_width, CAPTION_HEIGHT)
# The hexagon and digits are drawn from the node's own origin, so the
# number is positioned rather than sized.
_numbers[i].position = Vector2(x, CAPTION_HEIGHT + CAPTION_GAP)
if _gas_caption != null:
_gas_caption.position = Vector2(size.x - 110.0, 2.0)
_gas_caption.size = Vector2(106.0, CAPTION_HEIGHT)
_gas_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
if _gas_icon != null:
_gas_icon.position = Vector2(size.x - 34.0, 0.0)
_gas_icon.size = Vector2(32.0, 32.0)
_gas_caption.size.x = maxf(0.0, _gas_caption.size.x - 36.0)
if _header_labels.is_empty():
return
var resource_width: float = size.x * 0.25
var value_width: float = (size.x - resource_width) / 3.0
var columns: Array = [0.0, resource_width, resource_width + value_width,
resource_width + value_width * 2.0]
var widths: Array = [resource_width, value_width, value_width, value_width]
for i in range(_header_labels.size()):
_header_labels[i].position = Vector2(columns[i], 0.0)
_header_labels[i].size = Vector2(widths[i], HEADER_HEIGHT)
var row_height: float = (size.y - HEADER_HEIGHT - 4.0) / maxf(1.0, float(_rows.size()))
var row_index: int = 0
for resource_type in GameState.RESOURCE_TYPES:
var row: Dictionary = _rows[resource_type]
var y: float = HEADER_HEIGHT + float(row_index) * row_height
row.glyph.position = Vector2(2.0, y + 2.0)
row.glyph.size = Vector2(row_height - 4.0, row_height - 4.0)
row.name.position = Vector2(row_height, y)
row.name.size = Vector2(resource_width - row_height, row_height)
for i in range(3):
var label: Label = row[["bank", "fleet", "local"][i]]
label.position = Vector2(columns[i + 1], y)
label.size = Vector2(widths[i + 1], row_height)
row_index += 1
queue_redraw()
func _process(_delta: float) -> void:
# Resource rows scale without inventing a new menu per resource. The old
# ore-only columns remain behind this concise, explicit summary.
_summary.text = "ORE Bank %d Fleet %d Local %d\nGAS Bank %d Fleet %d Local %d" % [GameState.rocks, GameState.fleet_cargo, GameState.building_holdings(building_id), GameState.gas, GameState.fleet_gas_cargo, GameState.building_gas_holdings(building_id)]
for number in _numbers: number.visible = false
for caption in _captions: caption.visible = false
_gas_caption.visible = false
_gas_icon.visible = false
_gas_caption.text = "GAS %d / %d / %d" % [GameState.gas, GameState.fleet_gas_cargo, GameState.building_gas_holdings(building_id)]
_numbers[0].set_amount(GameState.rocks)
_numbers[1].set_amount(GameState.fleet_cargo)
# A building with no id is not a building: showing "0" there would claim
# the store is empty rather than that there is no store to look at.
var known: bool = building_id != 0
_numbers[2].visible = known
_captions[2].visible = known
if known:
_numbers[2].set_amount(GameState.building_holdings(building_id))
for resource_type in GameState.RESOURCE_TYPES:
var row: Dictionary = _rows.get(resource_type, {})
if row.is_empty():
continue
row.bank.text = str(GameState.banked_resource(resource_type))
row.fleet.text = str(GameState.fleet_resource(resource_type))
row.local.text = str(GameState.building_resource(building_id, resource_type)) \
if building_id != 0 else "—"
# The height the bar needs for a caption line and a row of digits. The scene
# sizes the node, so this is what those offsets have to add up to; anything
# less and the rule is drawn back through the digits.
static func natural_height() -> float:
return CAPTION_HEIGHT + CAPTION_GAP + DIGIT_HEIGHT + 8.0
return 84.0
func _draw() -> void:
# One rule under the whole bar, so the header reads as a band separating
# the standing figures from whatever the panel below them does.
var rule_y: float = maxf(size.y, natural_height())
NeonDraw.draw_glowing_line(
self, Vector2(0.0, rule_y), Vector2(size.x, rule_y), RULE_COLOR, 0.55, 1.0
)
NeonDraw.draw_glowing_line(self, Vector2(0.0, size.y), Vector2(size.x, size.y),
RULE_COLOR, 0.55, 1.0)

54
ResourceGlyph.gd Normal file
View file

@ -0,0 +1,54 @@
class_name ResourceGlyph
extends Control
# A small, non-interactive resource mark shared by tables and transfer rows.
# Resource buttons used to borrow IconButton, which made decorative icons
# look tappable and encouraged each screen to invent its own version.
@export var resource_type: String = GameState.RESOURCE_ORE: set = set_resource_type
func set_resource_type(value: String) -> void:
if resource_type == value:
return
resource_type = value
queue_redraw()
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
queue_redraw()
func _draw() -> void:
var colour: Color = GameState.resource_color(resource_type)
var centre: Vector2 = size * 0.5
var radius: float = minf(size.x, size.y) * 0.34
match resource_type:
GameState.RESOURCE_GAS:
_draw_gas(centre, radius, colour)
_:
_draw_ore(centre, radius, colour)
func _draw_ore(centre: Vector2, radius: float, colour: Color) -> void:
var points := PackedVector2Array()
for i in range(6):
var angle: float = TAU * float(i) / 6.0 - PI * 0.5
points.append(centre + Vector2(cos(angle), sin(angle)) * radius)
NeonDraw.draw_glowing_polygon(self, points, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035))
NeonDraw.draw_glowing_line(self, points[5], points[2], colour, 0.55, maxf(0.4, radius * 0.025), colour.darkened(0.78))
func _draw_gas(centre: Vector2, radius: float, colour: Color) -> void:
var bubbles: Array = [
[centre + Vector2(-radius * 0.38, radius * 0.12), radius * 0.46],
[centre + Vector2(radius * 0.34, radius * 0.26), radius * 0.58],
[centre + Vector2(radius * 0.38, -radius * 0.48), radius * 0.32],
]
for bubble in bubbles:
var ring := PackedVector2Array()
for i in range(12):
var angle: float = TAU * float(i) / 12.0
ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1]))
NeonDraw.draw_glowing_polygon(self, ring, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035))

161
ResourceTransferList.gd Normal file
View file

@ -0,0 +1,161 @@
class_name ResourceTransferList
extends Control
# Two-sided cargo control. Resources are rows and locations stay fixed:
# fleet on the left, the current building on the right. Horizontal arrows
# make the result of a press visible before the player commits to it.
signal transfer_requested(resource_type: String, to_building: bool)
const HEADER_HEIGHT: float = 58.0
const FLEET_COLOR: Color = Color(1.0, 0.78, 0.25)
const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55)
const RULE_COLOR: Color = Color(0.18, 0.38, 0.52)
var building_id: int = 0
var _destination_heading: Label
var _rows: Dictionary = {}
func _ready() -> void:
var fleet_heading := _make_label("FLEET HOLD", 22, FLEET_COLOR)
fleet_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(fleet_heading)
_destination_heading = _make_label("THIS BUILDING", 22, LOCAL_COLOR)
_destination_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(_destination_heading)
for resource_type in GameState.RESOURCE_TYPES:
var colour: Color = GameState.resource_color(resource_type)
var glyph := ResourceGlyph.new()
glyph.resource_type = resource_type
add_child(glyph)
var name_label := _make_label(GameState.resource_name(resource_type).to_upper(), 25, colour)
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(name_label)
var fleet_amount := _make_label("0", 42, FLEET_COLOR)
fleet_amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(fleet_amount)
var local_amount := _make_label("0", 42, LOCAL_COLOR)
local_amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(local_amount)
var unload := IconButton.new()
unload.name = "%sToBuildingButton" % GameState.resource_name(resource_type)
unload.icon = IconButton.Icon.RIGHT_ARROW
unload.glow_color = colour
unload.pressed.connect(_request_transfer.bind(resource_type, true))
add_child(unload)
var unload_label := _make_label("UNLOAD ALL →", 20, colour)
unload_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(unload_label)
var load := IconButton.new()
load.name = "%sToFleetButton" % GameState.resource_name(resource_type)
load.icon = IconButton.Icon.LEFT_ARROW
load.glow_color = colour
load.pressed.connect(_request_transfer.bind(resource_type, false))
add_child(load)
var load_label := _make_label("← LOAD ALL", 20, colour)
load_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(load_label)
_rows[resource_type] = {
"glyph": glyph, "name": name_label,
"fleet_amount": fleet_amount, "local_amount": local_amount,
"unload": unload, "unload_label": unload_label,
"load": load, "load_label": load_label,
}
resized.connect(_layout)
_layout()
refresh()
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
var label := Label.new()
label.text = text_value
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", colour)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
return label
func _request_transfer(resource_type: String, to_building: bool) -> void:
transfer_requested.emit(resource_type, to_building)
func set_destination(id: int, heading: String) -> void:
building_id = id
if _destination_heading != null:
_destination_heading.text = heading.to_upper()
refresh()
func refresh() -> void:
for resource_type in GameState.RESOURCE_TYPES:
var row: Dictionary = _rows.get(resource_type, {})
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)
row.fleet_amount.text = str(fleet_amount)
row.local_amount.text = str(local_amount)
row.unload.enabled = fleet_amount > 0
row.load.enabled = local_amount > 0
row.unload.queue_redraw()
row.load.queue_redraw()
func _layout() -> void:
if _destination_heading == null:
return
var side_width: float = size.x * 0.36
var centre_width: float = size.x - side_width * 2.0
var right_x: float = side_width + centre_width
var fleet_heading: Label = get_child(0)
fleet_heading.position = Vector2(0.0, 0.0)
fleet_heading.size = Vector2(side_width, HEADER_HEIGHT)
_destination_heading.position = Vector2(right_x, 0.0)
_destination_heading.size = Vector2(side_width, HEADER_HEIGHT)
var row_height: float = (size.y - HEADER_HEIGHT) / maxf(1.0, float(_rows.size()))
var row_index: int = 0
for resource_type in GameState.RESOURCE_TYPES:
var row: Dictionary = _rows[resource_type]
var y: float = HEADER_HEIGHT + float(row_index) * row_height
var glyph_size: float = minf(58.0, row_height * 0.22)
row.glyph.position = Vector2(size.x * 0.5 - glyph_size * 0.5, y + 12.0)
row.glyph.size = Vector2(glyph_size, glyph_size)
row.name.position = Vector2(side_width, y + 12.0 + glyph_size)
row.name.size = Vector2(centre_width, 38.0)
row.fleet_amount.position = Vector2(0.0, y + 12.0)
row.fleet_amount.size = Vector2(side_width, 62.0)
row.local_amount.position = Vector2(right_x, y + 12.0)
row.local_amount.size = Vector2(side_width, 62.0)
var button_size: float = minf(118.0, row_height * 0.42)
var button_y: float = y + 78.0
row.unload.position = Vector2(side_width * 0.5 - button_size * 0.5, button_y)
row.unload.size = Vector2(button_size, button_size)
row.load.position = Vector2(right_x + side_width * 0.5 - button_size * 0.5, button_y)
row.load.size = Vector2(button_size, button_size)
var caption_y: float = button_y + button_size + 2.0
row.unload_label.position = Vector2(0.0, caption_y)
row.unload_label.size = Vector2(side_width, 36.0)
row.load_label.position = Vector2(right_x, caption_y)
row.load_label.size = Vector2(side_width, 36.0)
row_index += 1
queue_redraw()
func _draw() -> void:
var row_height: float = (size.y - HEADER_HEIGHT) / maxf(1.0, float(_rows.size()))
NeonDraw.draw_glowing_line(self, Vector2(0.0, HEADER_HEIGHT), Vector2(size.x, HEADER_HEIGHT),
RULE_COLOR, 0.5, 0.7)
for i in range(1, _rows.size()):
var y: float = HEADER_HEIGHT + float(i) * row_height
NeonDraw.draw_glowing_line(self, Vector2(0.0, y), Vector2(size.x, y),
RULE_COLOR, 0.4, 0.6)

View file

@ -1,8 +1,7 @@
extends Node2D
# Preview, not a test. The resource readouts with the fleet carrying ore, so
# the #15 banked-vs-carried split can actually be looked at - headless draws
# no pixels and cannot say whether two numbers read as two different things.
# Preview, not a test. Both banked resources and both fleet-held resources
# are populated so the complete two-column HUD can be inspected.
#
# bash tests/screenshot.sh res://tests/_cargo_readout_preview.tscn /tmp/hud.png 20 720x1280
@ -11,5 +10,7 @@ func _ready() -> void:
GameState.buildings.clear()
GameState.extractors.clear()
GameState.rocks = 128
GameState.gas = 46
GameState.fleet_cargo = 34
GameState.fleet_gas_cargo = 17
add_child(load("res://Main.tscn").instantiate())

View file

@ -13,13 +13,16 @@ func _ready() -> void:
GameState.extractors.clear()
GameState.transporters.clear()
GameState.rocks = 180
GameState.gas = 36
GameState.fleet_cargo = 24
GameState.fleet_gas_cargo = 12
GameState.unlocked = ["shipyards", "logistics", "freight"]
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)
main.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0))
var crafter_id: int = int(GameState.buildings[-1].id)
GameState.deposit_to_building(crafter_id, 42)
GameState.find_building(crafter_id).gas_stock = 18
var bc: Node = main.get_node("BuildController")
bc.craft_building_id = crafter_id
# The shared header (#24) is the thing worth eyeballing here, so give it

View file

@ -0,0 +1,14 @@
extends Node2D
func _ready() -> void:
GameState.planet_name = "Violet Tempest"
GameState.planet_type = "gas_giant"
GameState.surviving_ship_count = 3
GameState.rocks = 128
GameState.gas = 46
GameState.fleet_cargo = 34
GameState.fleet_gas_cargo = 17
var surface: Node = load("res://PlanetSurface.tscn").instantiate()
surface.get_node("SurfaceController").fade_duration = 0.0
add_child(surface)

View file

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

View file

@ -13,7 +13,9 @@ func _ready() -> void:
GameState.extractors.clear()
GameState.transporters.clear()
GameState.rocks = 180
GameState.gas = 36
GameState.fleet_cargo = 24
GameState.fleet_gas_cargo = 12
GameState.unlocked = ["shipyards", "logistics", "freight"]
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)

View file

@ -28,18 +28,15 @@ WAIT_FRAMES="${3:-60}"
RESOLUTION="${4:-1280x720}"
proot-distro login ubuntu -- bash -c "
# A killed run (or a session that ended mid-screenshot) leaves /tmp/.X99-lock
# behind, and Xvfb then refuses to claim :99 - which surfaces much later and
# far less obviously as Godot's 'X11 Display is not available'. Nothing else
# uses :99 here, so a lock with no live Xvfb behind it is always stale.
if ! pgrep -x Xvfb >/dev/null; then
rm -f /tmp/.X99-lock /tmp/.X11-unix/X99
fi
# Display :99 is dedicated to this helper. Clear both a live process left by
# an interrupted capture and its lock/socket before claiming it again.
pkill -x Xvfb >/dev/null 2>&1 || true
rm -f /tmp/.X99-lock /tmp/.X11-unix/X99
Xvfb :99 -screen 0 ${RESOLUTION}x24 &
XVFB_PID=\$!
trap 'kill \$XVFB_PID >/dev/null 2>&1 || true' EXIT
sleep 2
mkdir -p /tmp/xdgrt && chmod 700 /tmp/xdgrt
cd '$PROJECT_DIR'
DISPLAY=:99 XDG_RUNTIME_DIR=/tmp/xdgrt GALLIUM_DRIVER=softpipe '$GODOT_BIN' --rendering-driver opengl3 --rendering-method gl_compatibility --resolution '$RESOLUTION' -s res://tests/_screenshot_tool.gd -- '$SCENE_PATH' '$OUTPUT_PATH' '$WAIT_FRAMES'
kill \$XVFB_PID
DISPLAY=:99 XDG_RUNTIME_DIR=/tmp/xdgrt GALLIUM_DRIVER=softpipe '$GODOT_BIN' --audio-driver Dummy --rendering-driver opengl3 --rendering-method gl_compatibility --resolution '$RESOLUTION' -s res://tests/_screenshot_tool.gd -- '$SCENE_PATH' '$OUTPUT_PATH' '$WAIT_FRAMES'
"

View file

@ -96,7 +96,10 @@ func _process(delta: float) -> bool:
print("TEST_CRAFTER_FEEDBACK: FAIL (fleet parked on the crafter never registered)")
quit(1)
return true
# A crafter now has both Craft and Transfer, so the shared action
# button unfolds those choices before the craft action is selected.
_tap(controller.action_button)
_tap(controller.building_action_button)
if not controller.craft_menu.visible:
print("TEST_CRAFTER_FEEDBACK: FAIL (the action button did not open the crafter panel)")
quit(1)

View file

@ -162,8 +162,11 @@ func _process(delta: float) -> bool:
stage = 6
return false
6:
# Long enough for the build itself, which #23 made take time.
if elapsed < game_state.build_time(GameState.PRODUCT_SHIP) * 3.0:
# Long enough for both construction and the physical delivery trip
# from this remote crafter to the mothership hangar.
var delivery_time: float = CRAFTER_AT.distance_to(MOTHERSHIP_AT) \
/ GameState.VESSEL_DELIVERY_SPEED
if elapsed < game_state.build_time(GameState.PRODUCT_SHIP) * 3.0 + delivery_time + 0.5:
return false
# --- 5. the queued order completed off the fleet's own ore ---
if game_state.pending_orders(crafter_id) != 0:

View file

@ -180,7 +180,7 @@ func _process(delta: float) -> bool:
# The handler working while the BUTTON is inert is the exact bug
# class this project keeps hitting, so one press goes the long way.
var bank_at_tap: int = game_state.rocks
_tap(scene.get_node("UI/TransferMenu/ToFleetAllButton"))
_tap(scene.get_node("UI/TransferMenu/ResourceList/OreToFleetButton"))
_expect(game_state.fleet_cargo == bank_at_tap,
"a real tap did not load all banked ore")
_expect(game_state.rocks == 0,