Add transfer amounts and active gas vents

This commit is contained in:
Loïc Wisniewski 2026-08-14 13:44:10 +02:00
commit e4df9bc41a
8 changed files with 147 additions and 12 deletions

View file

@ -2,12 +2,15 @@ extends Node2D
@onready var fleet: Node2D = get_node("../Fleet") @onready var fleet: Node2D = get_node("../Fleet")
var vents: Array[Vector2] = [] var vents: Array[Vector2] = []
var progress: float = 0.0 var progress: float = 0.0
var emission_phase: float = 0.0
func _ready() -> void: func _ready() -> void:
var rng := RandomNumberGenerator.new() var rng := RandomNumberGenerator.new()
rng.seed = hash(GameState.planet_name) rng.seed = hash(GameState.planet_name)
for i in range(6): vents.append(Vector2(rng.randf_range(-800,800), rng.randf_range(-650,650))) for i in range(6): vents.append(Vector2(rng.randf_range(-800,800), rng.randf_range(-650,650)))
queue_redraw() queue_redraw()
func _process(delta: float) -> void: func _process(delta: float) -> void:
emission_phase = fmod(emission_phase + delta * 0.55, 1.0)
queue_redraw()
for vent in vents: for vent in vents:
if fleet.get_centroid().distance_to(vent) < 90.0: if fleet.get_centroid().distance_to(vent) < 90.0:
progress += delta progress += delta
@ -19,6 +22,7 @@ func _process(delta: float) -> void:
func _draw() -> void: func _draw() -> void:
for vent in vents: for vent in vents:
_draw_gas_source(vent) _draw_gas_source(vent)
_draw_emission(vent)
func _draw_gas_source(centre: Vector2) -> void: func _draw_gas_source(centre: Vector2) -> void:
@ -35,3 +39,23 @@ func _draw_gas_source(centre: Vector2) -> void:
var angle: float = TAU * float(i) / 12.0 var angle: float = TAU * float(i) / 12.0
ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1])) ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1]))
NeonDraw.draw_glowing_polygon(self, ring, colour, 0.9, core, 0.6) NeonDraw.draw_glowing_polygon(self, ring, colour, 0.9, core, 0.6)
func _draw_emission(centre: Vector2) -> void:
# Small bubbles continuously rise out of every source. This remains
# animated even when the fleet is not collecting, so the vent reads as
# an active gas generator rather than a static decoration.
var core := Color(0.15, 0.03, 0.22)
for i in range(4):
var travel: float = fmod(emission_phase + float(i) * 0.25, 1.0)
var bubble_centre := centre + Vector2(
sin((travel + float(i)) * TAU) * 11.0,
-48.0 - travel * 92.0
)
var radius: float = lerpf(5.0, 14.0, travel)
var colour := Color(0.92, 0.68, 1.0, 1.0 - travel * 0.72)
var ring := PackedVector2Array()
for point in range(10):
var angle: float = TAU * float(point) / 10.0
ring.append(bubble_centre + Vector2(cos(angle), sin(angle)) * radius)
NeonDraw.draw_glowing_polygon(self, ring, colour, 0.75, core, 0.45)

View file

@ -28,6 +28,7 @@ enum Icon {
ACTION_MENU, ACTION_MENU,
GAS, GAS,
LEFT_ARROW, RIGHT_ARROW, LEFT_ARROW, RIGHT_ARROW,
MINUS, PLUS,
} }
# Size the glyphs were originally drawn against. Every glyph dimension is # Size the glyphs were originally drawn against. Every glyph dimension is
@ -208,6 +209,10 @@ func _draw() -> void:
_draw_horizontal_arrow(center, pulse, -1.0, s) _draw_horizontal_arrow(center, pulse, -1.0, s)
Icon.RIGHT_ARROW: Icon.RIGHT_ARROW:
_draw_horizontal_arrow(center, pulse, 1.0, s) _draw_horizontal_arrow(center, pulse, 1.0, s)
Icon.MINUS:
_draw_minus(center, pulse, s)
Icon.PLUS:
_draw_plus(center, pulse, s)
func _draw_close(center: Vector2, pulse: float, s: float) -> void: func _draw_close(center: Vector2, pulse: float, s: float) -> void:
@ -242,6 +247,19 @@ func _draw_horizontal_arrow(center: Vector2, pulse: float, direction: float, s:
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_minus(center: Vector2, pulse: float, s: float) -> void:
var r: float = 9.0 * s
NeonDraw.draw_glowing_line(self, center + Vector2(-r, 0.0), center + Vector2(r, 0.0),
_active_color(), pulse, 0.55 * s, core_color)
func _draw_plus(center: Vector2, pulse: float, s: float) -> void:
_draw_minus(center, pulse, s)
var r: float = 9.0 * s
NeonDraw.draw_glowing_line(self, center + Vector2(0.0, -r), center + Vector2(0.0, r),
_active_color(), pulse, 0.55 * s, core_color)
func _draw_transfer_icon(center: Vector2, pulse: float, s: float) -> void: func _draw_transfer_icon(center: Vector2, pulse: float, s: float) -> void:
# Two shafts side by side pointing opposite ways. Drawn as a pair rather # 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 # than as one double-headed arrow because the button's whole job is that

View file

@ -1522,7 +1522,7 @@ offset_left = 24
offset_top = 1092 offset_top = 1092
offset_right = -24 offset_right = -24
offset_bottom = 1150 offset_bottom = 1150
text = "Arrows move everything available on that side" text = "Arrows move the selected amount from that side"
horizontal_alignment = 1 horizontal_alignment = 1
theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1) theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1)
theme_override_font_sizes/font_size = 20 theme_override_font_sizes/font_size = 20

View file

@ -5,12 +5,14 @@ extends Control
# fleet on the left, the current building on the right. Horizontal arrows # 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. # make the result of a press visible before the player commits to it.
signal transfer_requested(resource_type: String, to_building: bool) signal transfer_requested(resource_type: String, to_building: bool, amount: int)
const HEADER_HEIGHT: float = 58.0 const HEADER_HEIGHT: float = 58.0
const FLEET_COLOR: Color = Color(1.0, 0.78, 0.25) const FLEET_COLOR: Color = Color(1.0, 0.78, 0.25)
const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55) const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55)
const RULE_COLOR: Color = Color(0.18, 0.38, 0.52) const RULE_COLOR: Color = Color(0.18, 0.38, 0.52)
const ALL_AMOUNT: int = 1 << 30
const AMOUNT_OPTIONS: Array[int] = [1, 5, 25, ALL_AMOUNT]
var building_id: int = 0 var building_id: int = 0
var _destination_heading: Label var _destination_heading: Label
@ -46,7 +48,7 @@ func _ready() -> void:
unload.glow_color = colour unload.glow_color = colour
unload.pressed.connect(_request_transfer.bind(resource_type, true)) unload.pressed.connect(_request_transfer.bind(resource_type, true))
add_child(unload) add_child(unload)
var unload_label := _make_label("UNLOAD ALL →", 20, colour) var unload_label := _make_label("UNLOAD →", 20, colour)
unload_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER unload_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(unload_label) add_child(unload_label)
@ -56,15 +58,37 @@ func _ready() -> void:
load.glow_color = colour load.glow_color = colour
load.pressed.connect(_request_transfer.bind(resource_type, false)) load.pressed.connect(_request_transfer.bind(resource_type, false))
add_child(load) add_child(load)
var load_label := _make_label("← LOAD ALL", 20, colour) var load_label := _make_label("← LOAD", 20, colour)
load_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER load_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(load_label) add_child(load_label)
var decrease := IconButton.new()
decrease.name = "%sDecreaseButton" % GameState.resource_name(resource_type)
decrease.icon = IconButton.Icon.MINUS
decrease.glow_color = colour
decrease.pressed.connect(_change_amount.bind(resource_type, -1))
add_child(decrease)
var amount_label := _make_label("ALL", 24, colour)
amount_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(amount_label)
var increase := IconButton.new()
increase.name = "%sIncreaseButton" % GameState.resource_name(resource_type)
increase.icon = IconButton.Icon.PLUS
increase.glow_color = colour
increase.pressed.connect(_change_amount.bind(resource_type, 1))
add_child(increase)
var amount_caption := _make_label("AMOUNT", 16, Color(0.62, 0.78, 0.86))
amount_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(amount_caption)
_rows[resource_type] = { _rows[resource_type] = {
"glyph": glyph, "name": name_label, "glyph": glyph, "name": name_label,
"fleet_amount": fleet_amount, "local_amount": local_amount, "fleet_amount": fleet_amount, "local_amount": local_amount,
"unload": unload, "unload_label": unload_label, "unload": unload, "unload_label": unload_label,
"load": load, "load_label": load_label, "load": load, "load_label": load_label,
"decrease": decrease, "amount_label": amount_label,
"increase": increase, "amount_caption": amount_caption,
"amount_index": AMOUNT_OPTIONS.size() - 1,
} }
resized.connect(_layout) resized.connect(_layout)
@ -83,7 +107,29 @@ func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
func _request_transfer(resource_type: String, to_building: bool) -> void: func _request_transfer(resource_type: String, to_building: bool) -> void:
transfer_requested.emit(resource_type, to_building) var row: Dictionary = _rows.get(resource_type, {})
if row.is_empty():
return
transfer_requested.emit(resource_type, to_building,
AMOUNT_OPTIONS[int(row.amount_index)])
func _change_amount(resource_type: String, direction: int) -> void:
var row: Dictionary = _rows.get(resource_type, {})
if row.is_empty():
return
row.amount_index = clampi(int(row.amount_index) + direction, 0, AMOUNT_OPTIONS.size() - 1)
_refresh_amount_controls(row)
func _refresh_amount_controls(row: Dictionary) -> void:
var index: int = int(row.amount_index)
var amount: int = AMOUNT_OPTIONS[index]
row.amount_label.text = "ALL" if amount == ALL_AMOUNT else str(amount)
row.decrease.enabled = index > 0
row.increase.enabled = index < AMOUNT_OPTIONS.size() - 1
row.decrease.queue_redraw()
row.increase.queue_redraw()
func set_destination(id: int, heading: String) -> void: func set_destination(id: int, heading: String) -> void:
@ -106,6 +152,7 @@ func refresh() -> void:
row.load.enabled = local_amount > 0 row.load.enabled = local_amount > 0
row.unload.queue_redraw() row.unload.queue_redraw()
row.load.queue_redraw() row.load.queue_redraw()
_refresh_amount_controls(row)
func _layout() -> void: func _layout() -> void:
@ -147,6 +194,17 @@ func _layout() -> void:
row.unload_label.size = Vector2(side_width, 36.0) row.unload_label.size = Vector2(side_width, 36.0)
row.load_label.position = Vector2(right_x, caption_y) row.load_label.position = Vector2(right_x, caption_y)
row.load_label.size = Vector2(side_width, 36.0) row.load_label.size = Vector2(side_width, 36.0)
var selector_size: float = minf(54.0, centre_width * 0.29)
var selector_y: float = y + 126.0
row.decrease.position = Vector2(side_width + 2.0, selector_y)
row.decrease.size = Vector2(selector_size, selector_size)
row.increase.position = Vector2(right_x - selector_size - 2.0, selector_y)
row.increase.size = Vector2(selector_size, selector_size)
row.amount_label.position = Vector2(side_width + selector_size + 2.0, selector_y)
row.amount_label.size = Vector2(centre_width - selector_size * 2.0 - 4.0, selector_size)
row.amount_caption.position = Vector2(side_width, selector_y + selector_size + 2.0)
row.amount_caption.size = Vector2(centre_width, 30.0)
row_index += 1 row_index += 1
queue_redraw() queue_redraw()

View file

@ -26,6 +26,12 @@ var rng: RandomNumberGenerator = RandomNumberGenerator.new()
func _ready() -> void: func _ready() -> void:
# Gas giants have their own gas vents and must never receive ore pickups.
# This check happens before the initial population pass, unlike merely
# hiding the field later from PlanetSurfaceController.
if GameState.planet_type == "gas_giant":
set_process(false)
return
rng.randomize() rng.randomize()
# Populate immediately rather than waiting out the first interval, so # Populate immediately rather than waiting out the first interval, so
# the surface isn't visibly empty for half a second on arrival. # the surface isn't visibly empty for half a second on arrival.
@ -65,7 +71,7 @@ func _spawn_rock(centre: Vector2) -> void:
var rock: Area2D = rock_scene.instantiate() var rock: Area2D = rock_scene.instantiate()
rock.position = centre + Vector2(cos(angle), sin(angle)) * distance rock.position = centre + Vector2(cos(angle), sin(angle)) * distance
rock.build_shape(rng.randi()) rock.build_shape(rng.randi())
# Deferred: update_rocks() runs from _ready(), and Godot refuses a # Keep pickups under this field so its visibility and lifecycle really
# direct add_child() while the tree is still setting up children. # own them. Deferred is still required because this can run from _ready().
get_parent().add_child.call_deferred(rock) add_child.call_deferred(rock)
rocks.append(rock) rocks.append(rock)

View file

@ -27,4 +27,5 @@ func _ready() -> void:
bc.transfer_building_id = crafter_id bc.transfer_building_id = crafter_id
main.get_node("UI/TransferMenu").visible = true main.get_node("UI/TransferMenu").visible = true
bc._refresh_transfer() bc._refresh_transfer()
bc.transfer_list._change_amount(GameState.RESOURCE_ORE, -1)
bc.transfer_status.text = "Moved 25 ore into this building" bc.transfer_status.text = "Moved 25 ore into this building"

View file

@ -33,6 +33,23 @@ func _process(delta: float) -> bool:
if nearby_position.length() > WorldGen.CHUNK_SIZE * 2.0: if nearby_position.length() > WorldGen.CHUNK_SIZE * 2.0:
return _fail("nearby gas giant spawned too far away: %s" % nearby_position) return _fail("nearby gas giant spawned too far away: %s" % nearby_position)
# Gas surfaces have vents only: no deferred ore pickups, and the vent
# emission advances continuously so the source visibly looks active.
game_state.planet_name = "Gas Test"
game_state.planet_type = "gas_giant"
var surface: Node = load("res://PlanetSurface.tscn").instantiate()
root.add_child(surface)
var rock_field: Node = surface.get_node("RockField")
var gas_field: Node = surface.get_node("GasVentField")
if not rock_field.rocks.is_empty() or rock_field.is_processing():
surface.free()
return _fail("gas surface spawned or continued processing ore rocks")
gas_field._process(0.5)
if gas_field.emission_phase <= 0.0:
surface.free()
return _fail("gas-source emission animation did not advance")
surface.free()
var found_giant: bool = false var found_giant: bool = false
for x in range(-12, 13): for x in range(-12, 13):
for y in range(-12, 13): for y in range(-12, 13):

View file

@ -180,11 +180,22 @@ func _process(delta: float) -> bool:
# The handler working while the BUTTON is inert is the exact bug # The handler working while the BUTTON is inert is the exact bug
# class this project keeps hitting, so one press goes the long way. # class this project keeps hitting, so one press goes the long way.
var bank_at_tap: int = game_state.rocks var bank_at_tap: int = game_state.rocks
_tap(scene.get_node("UI/TransferMenu/ResourceList/OreToFleetButton")) var resource_list: Node = scene.get_node("UI/TransferMenu/ResourceList")
# Default is ALL. One minus selects 25, proving the amount control
# affects a real transfer rather than only changing its caption.
_tap(resource_list.get_node("OreDecreaseButton"))
_tap(resource_list.get_node("OreToFleetButton"))
_expect(game_state.fleet_cargo == mini(GameState.CARGO_LOAD_BATCH, bank_at_tap),
"selected 25 but the real tap loaded %d" % game_state.fleet_cargo)
_expect(game_state.rocks == bank_at_tap - game_state.fleet_cargo,
"the bank did not pay the selected transfer amount")
# Back to ALL and move the remainder, preserving the original
# end-to-end all-transfer coverage as well.
_tap(resource_list.get_node("OreIncreaseButton"))
_tap(resource_list.get_node("OreToFleetButton"))
_expect(game_state.fleet_cargo == bank_at_tap, _expect(game_state.fleet_cargo == bank_at_tap,
"a real tap did not load all banked ore") "switching back to ALL did not load the remainder")
_expect(game_state.rocks == 0, _expect(game_state.rocks == 0, "ALL left ore in the bank")
"the bank did not pay for the tapped withdrawal")
# --- 7. conservation ---------------------------------------- # --- 7. conservation ----------------------------------------
_expect(_total() == total_before, _expect(_total() == total_before,