diff --git a/BuildController.gd b/BuildController.gd index 8ac9ed7..6cb587c 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -74,12 +74,34 @@ var lab_choice: int = 0 @onready var craft_menu: Control = get_node("../UI/CraftMenu") @onready var craft_interceptor_button: IconButton = get_node("../UI/CraftMenu/InterceptorButton") @onready var craft_transporter_button: IconButton = get_node("../UI/CraftMenu/TransporterButton") -@onready var craft_store_button: IconButton = get_node("../UI/CraftMenu/StoreOreButton") -@onready var craft_store_label: Label = get_node("../UI/CraftMenu/StoreOreLabel") @onready var craft_close_button: IconButton = get_node("../UI/CraftMenu/CloseButton") @onready var craft_status: Label = get_node("../UI/CraftMenu/StatusLabel") -@onready var load_ore_button: IconButton = get_node("../UI/SkillMenu/LoadOreButton") -@onready var load_ore_label: Label = get_node("../UI/SkillMenu/LoadOreLabel") + +# --- the transfer panel (#24) --------------------------------------------- +# Ore used to be moved from inside whichever panel happened to be open: "load +# ore" lived in the research screen, "store ore here" in the ship crafter. +# Both are now one top-level button that works at every building and in both +# directions, so moving ore is never buried under an unrelated menu. +@onready var transfer_button: IconButton = get_node("../UI/TransferButton") +@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_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_button: IconButton = get_node("../UI/TransferMenu/ToFleetAllButton") + +# 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"), +] @onready var skill_menu: Control = get_node("../UI/SkillMenu") @onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree") @@ -141,6 +163,12 @@ var craft_building_id: int = 0 var craft_message: String = "" var craft_message_until: int = 0 +# Which building the transfer panel is moving ore in and out of. Remembered +# for the same reason craft_building_id is: _process() clears +# 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 current_building: Node2D = null var pending_building: Node2D = null var pending_time: float = 0.0 @@ -167,11 +195,16 @@ func _ready() -> void: action_button.pressed.connect(_on_action_pressed) craft_interceptor_button.pressed.connect(_on_craft_interceptor) - load_ore_button.pressed.connect(_on_load_ore) craft_transporter_button.pressed.connect(_on_craft_transporter) - craft_store_button.pressed.connect(_on_store_ore) craft_close_button.pressed.connect(func(): craft_menu.visible = false) + transfer_button.pressed.connect(_on_transfer_pressed) + 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)) + formation_close_button.pressed.connect(func(): formation_menu.visible = false) formation_reset_button.pressed.connect(_on_reset_formation) formation_editor.formation_changed.connect(_on_formation_changed) @@ -186,14 +219,16 @@ func _ready() -> void: formation_menu.visible = false skill_menu.visible = false lab_menu.visible = false + transfer_menu.visible = false action_button.visible = false + transfer_button.visible = false # True while a building prompt or panel owns the shared action slot, so the # enter-atmosphere prompt knows to stay out of the way. func is_showing_building_prompt() -> bool: return current_building != null or build_menu.visible or craft_menu.visible \ - or formation_menu.visible or skill_menu.visible + or formation_menu.visible or skill_menu.visible or transfer_menu.visible func _unhandled_input(event: InputEvent) -> void: @@ -218,7 +253,8 @@ func _unhandled_input(event: InputEvent) -> void: formation_editor.handle_drag(event) get_viewport().set_input_as_handled() return - if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible: + if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible \ + or transfer_menu.visible: # These panels have no raw-input handler of their own, but the world # behind them must still not react. get_viewport().set_input_as_handled() @@ -239,11 +275,18 @@ func _process(delta: float) -> void: # the queue down instead of looking stuck. _refresh_craft_status() + if transfer_menu.visible: + # Also live: a hauler banking ore while the panel is open should make + # the "to fleet" side become available without a reopen. + _refresh_transfer() + if world_map.visible or build_menu.visible or craft_menu.visible \ - or formation_menu.visible or skill_menu.visible or lab_menu.visible: + or formation_menu.visible or skill_menu.visible or lab_menu.visible \ + or transfer_menu.visible: current_building = null pending_building = null action_button.visible = false + transfer_button.visible = false return # Debounced exactly like the enter-atmosphere prompt, so the action @@ -261,6 +304,10 @@ func _process(delta: float) -> void: current_building = pending_building action_button.visible = current_building != null + # Every building can hold ore, so the transfer button rides alongside the + # action button rather than being one of the things the action button + # turns into - the two are independent choices at the same structure. + transfer_button.visible = current_building != null if current_building != null: action_button.icon = _action_icon_for(current_building.building_type) action_button.queue_redraw() @@ -292,9 +339,14 @@ func _check_fleet_wipe() -> void: func _on_action_pressed() -> void: if current_building == null: return + # Whichever panel opens, its header reports on the building it was opened + # from. Set once here rather than in each branch, so a new building menu + # cannot forget to do it and silently show a blank HERE column. + for bar in resource_bars: + bar.building_id = current_building.building_id + if current_building.building_type == GameState.BUILDING_MOTHERSHIP: skill_menu.visible = true - _refresh_load_ore() _refresh_skill_menu() elif current_building.building_type == GameState.BUILDING_SHIP_CRAFTER: # Remembered explicitly: _process() clears current_building while any @@ -591,14 +643,14 @@ func _say(message: String) -> void: func _refresh_craft_status() -> void: - # "Here" is the ore delivered to THIS crafter, not the bank: since #16 a - # building builds out of its own stock, and showing the bank would say - # you can afford something the crafter cannot actually make. + # The bank/hold/here figures moved out to the shared header (#24), so this + # line is now only what is specific to a crafter: prices, the queue, and + # berth pressure. It used to restate "Here %d" itself, which is exactly + # the per-panel improvisation the header replaces. var queued: int = GameState.pending_orders(craft_building_id) - var stock: int = GameState.building_stock(craft_building_id) var queue_text: String = "" if queued == 0 else " Queued %d" % queued - craft_status.text = "Interceptor %d Transporter %d Here %d%s Transporters %d/%d" % [ - GameState.SHIP_COST, GameState.TRANSPORTER_COST, stock, queue_text, + craft_status.text = "Interceptor %d Transporter %d%s Transporters %d/%d" % [ + GameState.SHIP_COST, GameState.TRANSPORTER_COST, queue_text, GameState.transporters_claimed(), GameState.transport_capacity() ] # Nowhere to station one means the order would be refused, so say so @@ -609,12 +661,6 @@ func _refresh_craft_status() -> void: craft_shield_button.enabled = GameState.shield_vessel_unlocked() craft_shield_button.queue_redraw() - craft_store_label.text = "Store ore here\nHold %d" % GameState.fleet_cargo - # Deliberately left ENABLED with an empty hold: a greyed-out button that - # cannot say why is exactly the dead-button problem this panel already - # had. Pressing it explains instead. - craft_store_button.enabled = true - craft_store_button.queue_redraw() # A full fleet cannot take another vessel, so the order would be refused. # Showing that on the button is the difference between "not available yet" @@ -663,17 +709,6 @@ func _on_craft_interceptor() -> void: _report_order(GameState.PRODUCT_SHIP) -func _refresh_load_ore() -> void: - load_ore_label.text = "Load ore\nBank %d Hold %d" % [GameState.rocks, GameState.fleet_cargo] - load_ore_button.enabled = GameState.rocks > 0 - load_ore_button.queue_redraw() - - -func _on_load_ore() -> void: - GameState.withdraw_to_fleet() - _refresh_load_ore() - - func _report_order(product: String) -> void: var short: int = GameState.outstanding_cost(craft_building_id) if short > 0: @@ -684,17 +719,6 @@ func _report_order(product: String) -> void: _say("%s ordered - building now" % GameState.VESSEL_NAMES.get(product, product)) -func _on_store_ore() -> void: - if craft_building_id == 0: - _say("No crafter selected - reopen this panel from the building") - return - if GameState.fleet_cargo <= 0: - _say("The fleet is carrying nothing - load ore at the mothership first") - return - var moved: int = GameState.unload_into_building(craft_building_id) - _say("Stored %d ore here" % moved) - - func _on_craft_transporter() -> void: if craft_building_id == 0: _say("No crafter selected - reopen this panel from the building") @@ -823,6 +847,86 @@ func _on_craft_shield() -> void: _report_order(GameState.VESSEL_SHIELD) +# --- moving ore, in both directions, at any building (#24) ---------------- + +# Sentinel for the "All" buttons. The transfer primitives cap what they move +# by what is actually there, so asking for more than could possibly exist is +# how you say "everything" without the UI having to work out how much that +# is first - and the answer cannot go stale between the read and the press. +const ALL: int = 1 << 30 + + +func _on_transfer_pressed() -> void: + if current_building == null: + return + transfer_building_id = current_building.building_id + transfer_menu.visible = true + transfer_status.text = "" + _refresh_transfer() + + +func _on_transfer(to_building: bool, amount: int) -> 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) + + # 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" + else: + transfer_status.text = "%s has no ore to load" % _transfer_place_name() + elif to_building: + transfer_status.text = "Moved %d ore into %s" % [moved, _transfer_place_name()] + else: + transfer_status.text = "Loaded %d ore into the fleet" % moved + _refresh_transfer() + + +func _transfer_place_name() -> String: + var b: Dictionary = GameState.find_building(transfer_building_id) + if b.is_empty(): + return "this building" + if b.type == GameState.BUILDING_MOTHERSHIP: + # Worth naming as the bank rather than as a structure: this is the + # one building where unloading makes the ore spendable everywhere. + return "the bank" + return "this building" + + +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 = "Unload into %s" % _transfer_place_name() + to_fleet_label.text = "Load from %s" % _transfer_place_name() + # 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() + + func _on_formation_changed() -> void: fleet.apply_formation() diff --git a/BuildingField.gd b/BuildingField.gd index 4db4fac..496c26a 100644 --- a/BuildingField.gd +++ b/BuildingField.gd @@ -11,33 +11,20 @@ extends Node2D var building_scene: PackedScene = preload("res://Building.tscn") var spawned: Array = [] -# Whether the fleet was already standing on the mothership last frame - see -# the edge-triggered delivery in _process(). -var _was_on_mothership: bool = false + +# Nothing happens here per frame any more. Arriving at the mothership used to +# bank the fleet's hold automatically (#14), edge-triggered so that ore +# withdrawn while parked was not instantly taken back. #24 removes it +# entirely: unloading is now an explicit move in the transfer menu, in both +# directions and at every building, so an automatic one-way exception at the +# mothership was the odd case out - and it silently overrode a player who had +# just loaded the hold in order to carry ore somewhere else. func _ready() -> void: rebuild_all() -func _process(_delta: float) -> void: - # Unloading the fleet's hold (#14). Done here because this is the node - # that already knows where the buildings are and already answers "is the - # fleet standing on one?" - and it uses the fleet CENTROID, the same - # stable single-number test Planet and Building already use, rather than - # per-ship collision that flickers as ships jostle in formation. - # EDGE-triggered on arrival, not continuous. Delivering every frame the - # fleet is parked on the mothership would make withdrawing impossible: - # ore loaded into the hold at the mothership (#17) would be banked again - # the very next frame, and the button would look broken. - var under: Node2D = get_building_fleet_is_on() - var on_mothership: bool = under != null \ - and under.building_type == GameState.BUILDING_MOTHERSHIP - if on_mothership and not _was_on_mothership and GameState.fleet_cargo > 0: - GameState.deliver_cargo() - _was_on_mothership = on_mothership - - func rebuild_all() -> void: for b in spawned: if is_instance_valid(b): diff --git a/GameState.gd b/GameState.gd index 4dadbaa..25d796e 100644 --- a/GameState.gd +++ b/GameState.gd @@ -38,53 +38,70 @@ var rocks: int = 500 # surface would otherwise be dropped the moment the player took off. var fleet_cargo: int = 0 -signal cargo_delivered(amount: int) - - func collect_cargo(amount: int = 1) -> void: fleet_cargo += amount -# One press of "load ore" at the mothership. A batch rather than the whole -# bank: the hold is the part that can be lost, and emptying the bank in one -# tap would also strand research, which still spends from it. +# 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. const CARGO_LOAD_BATCH: int = 25 -func withdraw_to_fleet(amount: int = CARGO_LOAD_BATCH) -> int: - # Banked ore back into the hold, so the fleet can carry it to a building - # that is waiting on it (#17). - var taken: int = mini(amount, rocks) - if taken <= 0: - return 0 - rocks -= taken - fleet_cargo += taken - return taken +# --- moving ore by hand, in either direction (#24) -------------------------- +# +# One pair of primitives the transfer menu drives, rather than the two +# one-way special cases that used to live inside the skill and craft panels +# ("load ore" at the mothership, "store ore here" at a crafter). +# +# The mothership is deliberately not a special case in the UI: it is simply +# the building whose holdings ARE the bank. Depositing at one banks the ore, +# withdrawing from one draws on the bank, and every other building keeps its +# own stock (#16). That is what lets one menu serve every structure. -func unload_into_building(id: int) -> int: - # Everything the fleet is carrying, into this building's stock. Returns - # how much moved so a caller can tell the player something happened. - if fleet_cargo <= 0: +func building_holdings(id: int) -> int: + var b: Dictionary = find_building(id) + if b.is_empty(): return 0 - var moved: int = fleet_cargo - if deposit_to_building(id, moved) <= 0: + if b.type == BUILDING_MOTHERSHIP: + return rocks + return int(b.get("stock", 0)) + + +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 - fleet_cargo = 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 -func deliver_cargo() -> int: - # Everything the fleet is carrying, banked at once. Returns how much moved - # so a caller can react to a delivery actually having happened rather than - # to merely being in range. - if fleet_cargo <= 0: +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 delivered: int = fleet_cargo - rocks += delivered - fleet_cargo = 0 - cargo_delivered.emit(delivered) - return delivered + 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 const MOTHERSHIP_COST: int = 20 const SHIP_CRAFTER_COST: int = 12 diff --git a/IconButton.gd b/IconButton.gd index 825cc9b..efaa1ed 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -16,6 +16,10 @@ enum Icon { # and the command centre sharing a folded-map glyph and neither of them # looking anything like the structure they build. MOTHERSHIP, CRAFTER, COMMAND_CENTER, EXTRACTOR, TRANSPORT_HUB, SHIP_LAB, + # Moving ore between the fleet and a building (#24). Two arrows passing + # each other: the single up/down arrows already mean "load" and "store" + # one way each, and this button is the place where both live. + TRANSFER, } # Size the glyphs were originally drawn against. Every glyph dimension is @@ -147,6 +151,8 @@ func _draw() -> void: _draw_transport_hub_icon(center, pulse, s) Icon.SHIP_LAB: _draw_ship_lab_icon(center, pulse, s) + Icon.TRANSFER: + _draw_transfer_icon(center, pulse, s) func _draw_close(center: Vector2, pulse: float, s: float) -> void: @@ -172,6 +178,27 @@ 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_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 + # ore moves BOTH ways here, and a double-headed arrow reads as one thing + # sliding back and forth. + var r: float = 10.0 * s + var gap: float = 7.0 * s + for side in [-1.0, 1.0]: + # Left shaft points up (into the fleet), right shaft points down. + var direction: float = side + var x: float = gap * side + var tip: Vector2 = center + Vector2(x, r * direction) + var tail: Vector2 = center + Vector2(x, -r * direction) + NeonDraw.draw_glowing_line(self, tail, tip, _active_color(), pulse, 0.42 * s, core_color) + var head: float = 5.0 * s + NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-head, -head * direction), + _active_color(), pulse, 0.42 * s, core_color) + NeonDraw.draw_glowing_line(self, tip, tip + Vector2(head, -head * direction), + _active_color(), pulse, 0.42 * s, core_color) + + func _draw_build(center: Vector2, pulse: float, s: float) -> void: # A plus inside a frame - "add a structure". var r: float = 11.0 * s diff --git a/Main.tscn b/Main.tscn index fa9b08a..18793fa 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=20 format=3] +[gd_scene load_steps=21 format=3] [ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Fleet.gd" id="2"] @@ -23,6 +23,7 @@ [ext_resource type="Script" path="res://SkillTreeView.gd" id="21"] [ext_resource type="Script" path="res://CargoCounter.gd" id="22"] [ext_resource type="Script" path="res://TransporterField.gd" id="23"] +[ext_resource type="Script" path="res://ResourceBar.gd" id="24"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("5") @@ -144,6 +145,18 @@ offset_bottom = -572 script = ExtResource("10") icon = 2 +[node name="TransferButton" type="Control" parent="UI"] +visible = false +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 24 +offset_right = 172 +offset_top = -600 +offset_bottom = -452 +script = ExtResource("10") +icon = 14 +glow_color = Color(1, 0.78, 0.25, 1) + [node name="MapButton" type="Control" parent="UI"] anchor_left = 1.0 anchor_right = 1.0 @@ -633,19 +646,27 @@ color = Color(0.03, 0.03, 0.08, 0.94) mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 60 -offset_right = -24 -offset_bottom = 170 +offset_top = 36 +offset_right = -200 +offset_bottom = 166 text = "Ship Lab" horizontal_alignment = 1 +[node name="Bar" type="Control" parent="UI/LabMenu"] +mouse_filter = 2 +offset_left = 24 +offset_top = 180 +offset_right = 520 +offset_bottom = 260 +script = ExtResource("24") + [node name="VesselA" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 offset_left = -290 offset_right = -170 -offset_top = 190 -offset_bottom = 310 +offset_top = 274 +offset_bottom = 394 script = ExtResource("10") icon = 2 glow_color = Color(0.25, 0.85, 1, 1) @@ -655,8 +676,8 @@ anchor_left = 0.5 anchor_right = 0.5 offset_left = -60 offset_right = 60 -offset_top = 190 -offset_bottom = 310 +offset_top = 274 +offset_bottom = 394 script = ExtResource("10") icon = 2 glow_color = Color(0.45, 0.8, 1, 1) @@ -666,8 +687,8 @@ anchor_left = 0.5 anchor_right = 0.5 offset_left = 170 offset_right = 290 -offset_top = 190 -offset_bottom = 310 +offset_top = 274 +offset_bottom = 394 script = ExtResource("10") icon = 12 glow_color = Color(0.95, 0.55, 0.85, 1) @@ -676,9 +697,9 @@ glow_color = Color(0.95, 0.55, 0.85, 1) mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 318 +offset_top = 402 offset_right = -24 -offset_bottom = 380 +offset_bottom = 460 theme_override_font_sizes/font_size = 34 horizontal_alignment = 1 text = "" @@ -688,7 +709,7 @@ mouse_filter = 2 anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 32 -offset_top = 396 +offset_top = 470 offset_right = -32 offset_bottom = -470 theme_override_font_sizes/font_size = 30 @@ -788,20 +809,28 @@ color = Color(0.03, 0.03, 0.08, 0.94) mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 70 -offset_right = -24 -offset_bottom = 196 +offset_top = 36 +offset_right = -200 +offset_bottom = 166 text = "Build a ship" horizontal_alignment = 1 +[node name="Bar" type="Control" parent="UI/CraftMenu"] +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/CraftMenu"] mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 206 +offset_top = 274 offset_right = -24 -offset_bottom = 360 -theme_override_font_sizes/font_size = 40 +offset_bottom = 420 +theme_override_font_sizes/font_size = 36 autowrap_mode = 2 text = "" horizontal_alignment = 1 @@ -813,8 +842,8 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -234 offset_right = -86 -offset_top = -74 -offset_bottom = 74 +offset_top = 60 +offset_bottom = 208 script = ExtResource("10") icon = 2 @@ -826,8 +855,8 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -300 offset_right = -20 -offset_top = 100 -offset_bottom = 200 +offset_top = 216 +offset_bottom = 296 theme_override_font_sizes/font_size = 32 text = "Interceptor" horizontal_alignment = 1 @@ -839,8 +868,8 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = 86 offset_right = 234 -offset_top = -74 -offset_bottom = 74 +offset_top = 60 +offset_bottom = 208 script = ExtResource("10") icon = 12 glow_color = Color(0.95, 0.55, 0.85, 1) @@ -853,8 +882,8 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = 20 offset_right = 300 -offset_top = 96 -offset_bottom = 176 +offset_top = 216 +offset_bottom = 296 theme_override_font_sizes/font_size = 32 autowrap_mode = 2 text = "Transporter" @@ -867,8 +896,8 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -74 offset_right = 74 -offset_top = -260 -offset_bottom = -112 +offset_top = -200 +offset_bottom = -52 script = ExtResource("10") icon = 2 glow_color = Color(0.45, 0.8, 1, 1) @@ -881,41 +910,13 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -160 offset_right = 160 -offset_top = -104 -offset_bottom = -44 +offset_top = -44 +offset_bottom = 16 theme_override_font_sizes/font_size = 32 autowrap_mode = 2 text = "Shield Vessel" horizontal_alignment = 1 -[node name="StoreOreButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -74 -offset_top = -304 -offset_right = 74 -offset_bottom = -156 -script = ExtResource("10") -icon = 3 -glow_color = Color(1, 0.78, 0.25, 1) - -[node name="StoreOreLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -260 -offset_top = -148 -offset_right = 260 -offset_bottom = -24 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Store ore here" -horizontal_alignment = 1 - [node name="CloseButton" type="Control" parent="UI/CraftMenu"] anchor_left = 1.0 anchor_right = 1.0 @@ -926,6 +927,171 @@ offset_bottom = 172 script = ExtResource("10") icon = 1 +[node name="TransferMenu" type="Control" parent="UI" groups=["modal_menus"]] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Backdrop" type="ColorRect" parent="UI/TransferMenu"] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="UI/TransferMenu"] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 36 +offset_right = -200 +offset_bottom = 166 +text = "Move ore" +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_right = -24 +offset_bottom = 396 +theme_override_font_sizes/font_size = 34 +autowrap_mode = 2 +text = "" +horizontal_alignment = 1 + +[node name="ToBuildingLabel" type="Label" parent="UI/TransferMenu"] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 400 +offset_right = -24 +offset_bottom = 452 +theme_override_font_sizes/font_size = 34 +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 = 462 +offset_bottom = 610 +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 = 616 +offset_bottom = 668 +theme_override_font_sizes/font_size = 30 +text = "" +horizontal_alignment = 1 + +[node name="ToBuildingAllButton" type="Control" parent="UI/TransferMenu"] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 36 +offset_right = 184 +offset_top = 462 +offset_bottom = 610 +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 = -10 +offset_right = 230 +offset_top = 616 +offset_bottom = 668 +theme_override_font_sizes/font_size = 30 +text = "All" +horizontal_alignment = 1 + +[node name="ToFleetLabel" type="Label" parent="UI/TransferMenu"] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 694 +offset_right = -24 +offset_bottom = 746 +theme_override_font_sizes/font_size = 34 +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 = 36 +offset_right = 184 +offset_top = 756 +offset_bottom = 904 +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 = 910 +offset_bottom = 962 +theme_override_font_sizes/font_size = 30 +text = "All" +horizontal_alignment = 1 + +[node name="CloseButton" type="Control" parent="UI/TransferMenu"] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -172 +offset_right = -24 +offset_top = 24 +offset_bottom = 172 +script = ExtResource("10") +icon = 1 +glow_color = Color(1, 0.78, 0.25, 1) + [node name="FormationMenu" type="Control" parent="UI" groups=["modal_menus"]] visible = false mouse_filter = 2 @@ -1026,18 +1192,26 @@ color = Color(0.03, 0.03, 0.08, 0.94) mouse_filter = 2 anchor_right = 1.0 offset_left = 24.0 -offset_top = 70.0 -offset_right = -24.0 -offset_bottom = 196.0 +offset_top = 36.0 +offset_right = -200.0 +offset_bottom = 166.0 text = "Research" horizontal_alignment = 1 +[node name="Bar" type="Control" parent="UI/SkillMenu"] +mouse_filter = 2 +offset_left = 24.0 +offset_top = 180.0 +offset_right = 520.0 +offset_bottom = 260.0 +script = ExtResource("24") + [node name="Tree" type="Control" parent="UI/SkillMenu"] mouse_filter = 2 anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 24.0 -offset_top = 210.0 +offset_top = 274.0 offset_right = -24.0 offset_bottom = -620.0 script = ExtResource("21") @@ -1096,34 +1270,6 @@ script = ExtResource("10") icon = 5 glow_color = Color(0.45, 0.9, 0.55, 1) -[node name="LoadOreButton" type="Control" parent="UI/SkillMenu"] -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -256.0 -offset_top = -304.0 -offset_right = -108.0 -offset_bottom = -156.0 -script = ExtResource("10") -icon = 4 -glow_color = Color(1, 0.78, 0.25, 1) - -[node name="LoadOreLabel" type="Label" parent="UI/SkillMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -330.0 -offset_top = -150.0 -offset_right = -30.0 -offset_bottom = -14.0 -theme_override_font_sizes/font_size = 28 -horizontal_alignment = 1 -autowrap_mode = 2 -text = "Load ore" - [node name="CloseButton" type="Control" parent="UI/SkillMenu"] anchor_left = 1.0 anchor_right = 1.0 diff --git a/ResourceBar.gd b/ResourceBar.gd new file mode 100644 index 0000000..8668ea9 --- /dev/null +++ b/ResourceBar.gd @@ -0,0 +1,111 @@ +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. + +# 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 RULE_COLOR: Color = Color(0.25, 0.55, 0.7) + +var _numbers: Array[NeonNumber] = [] +var _captions: Array[Label] = [] + + +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) + + 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) + + _layout() + resized.connect(_layout) + + +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) + + +func _process(_delta: float) -> void: + _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)) + + +# 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 + + +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 + ) diff --git a/ResourceBar.gd.uid b/ResourceBar.gd.uid new file mode 100644 index 0000000..b48218f --- /dev/null +++ b/ResourceBar.gd.uid @@ -0,0 +1 @@ +uid://c3x1o7bh4tl5 diff --git a/shots/craft.png b/shots/craft.png new file mode 100644 index 0000000..ffdad1f Binary files /dev/null and b/shots/craft.png differ diff --git a/shots/craft.png.import b/shots/craft.png.import new file mode 100644 index 0000000..98efd15 --- /dev/null +++ b/shots/craft.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgs8khl5js3ip" +path="res://.godot/imported/craft.png-a8697b6db303e087da4d7b82087549c2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shots/craft.png" +dest_files=["res://.godot/imported/craft.png-a8697b6db303e087da4d7b82087549c2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/shots/lab.png b/shots/lab.png new file mode 100644 index 0000000..2bd1a2d Binary files /dev/null and b/shots/lab.png differ diff --git a/shots/lab.png.import b/shots/lab.png.import new file mode 100644 index 0000000..ecc7552 --- /dev/null +++ b/shots/lab.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://m1obuogb3oca" +path="res://.godot/imported/lab.png-3565548fdb7eb90f645dd6a0f6721400.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shots/lab.png" +dest_files=["res://.godot/imported/lab.png-3565548fdb7eb90f645dd6a0f6721400.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/shots/skill.png b/shots/skill.png new file mode 100644 index 0000000..a15c7ab Binary files /dev/null and b/shots/skill.png differ diff --git a/shots/skill.png.import b/shots/skill.png.import new file mode 100644 index 0000000..48ef873 --- /dev/null +++ b/shots/skill.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://btv6h40i8dvcw" +path="res://.godot/imported/skill.png-7e1397f0de9b8dc691fffdc4231115e7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shots/skill.png" +dest_files=["res://.godot/imported/skill.png-7e1397f0de9b8dc691fffdc4231115e7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/shots/transfer.png b/shots/transfer.png new file mode 100644 index 0000000..4143dcb Binary files /dev/null and b/shots/transfer.png differ diff --git a/shots/transfer.png.import b/shots/transfer.png.import new file mode 100644 index 0000000..61d121a --- /dev/null +++ b/shots/transfer.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b32illfmrcwlt" +path="res://.godot/imported/transfer.png-6949bd9dcafcd9892b4949e2a2071ddc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://shots/transfer.png" +dest_files=["res://.godot/imported/transfer.png-6949bd9dcafcd9892b4949e2a2071ddc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tests/_craft_menu_preview.gd b/tests/_craft_menu_preview.gd index 73dca1f..760aa1e 100644 --- a/tests/_craft_menu_preview.gd +++ b/tests/_craft_menu_preview.gd @@ -1,8 +1,8 @@ extends Node2D # Preview, not a test. Opens the CraftMenu so its layout can be checked on a -# real viewport - the mothership's "Load ore" label was falling off the bottom -# of shorter screens, which no headless test can see. +# real viewport - label collisions and things falling off the bottom of +# shorter screens are what no headless test can see. # # bash tests/screenshot.sh res://tests/_craft_menu_preview.tscn /tmp/m.png 20 720x1280 # ... and again at 720x960 to check a shorter screen format. @@ -17,11 +17,14 @@ func _ready() -> void: GameState.unlocked = ["shipyards", "logistics", "freight"] var main: Node = load("res://Main.tscn").instantiate() add_child(main) - main.get_node("BuildingField").place(GameState.BUILDING_TRANSPORT_HUB, Vector2(400.0, 0.0)) + 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) var bc: Node = main.get_node("BuildController") - bc.craft_building_id = 1 + bc.craft_building_id = crafter_id + # The shared header (#24) is the thing worth eyeballing here, so give it + # a building to report on - otherwise its HERE column hides itself. + for bar in bc.resource_bars: + bar.building_id = crafter_id main.get_node("UI/CraftMenu").visible = true - if "Craft" == "Craft": - bc._refresh_craft_status() - else: - bc._refresh_load_ore() + bc._refresh_craft_status() diff --git a/tests/_lab_menu_preview.gd b/tests/_lab_menu_preview.gd index 31db6d8..1818a14 100644 --- a/tests/_lab_menu_preview.gd +++ b/tests/_lab_menu_preview.gd @@ -16,10 +16,17 @@ func _ready() -> void: var main: Node = load("res://Main.tscn").instantiate() add_child(main) main.get_node("BuildingField").place(GameState.BUILDING_SHIP_LAB, Vector2(400.0, 0.0)) + var lab_id: int = int(GameState.buildings[-1].id) + GameState.deposit_to_building(lab_id, 18) + GameState.fleet_cargo = 12 # A level already bought, so the list shows both a researched and an # unresearched line rather than a uniform page of zeroes. GameState.vessel_levels["int_damage"] = 1 var bc: Node = main.get_node("BuildController") + # The shared header (#24) hides its HERE column when it has no building, + # so bind it the way opening the panel from the building does. + for bar in bc.resource_bars: + bar.building_id = lab_id main.get_node("UI/LabMenu").visible = true bc.lab_vessel = GameState.VESSEL_INTERCEPTOR bc._refresh_lab() diff --git a/tests/_skill_menu_preview.gd b/tests/_skill_menu_preview.gd index 6a876b1..907741e 100644 --- a/tests/_skill_menu_preview.gd +++ b/tests/_skill_menu_preview.gd @@ -1,8 +1,8 @@ extends Node2D # Preview, not a test. Opens the SkillMenu so its layout can be checked on a -# real viewport - the mothership's "Load ore" label was falling off the bottom -# of shorter screens, which no headless test can see. +# real viewport - the tree lost vertical room to the shared resource header +# (#24), and whether it still fits is a thing only pixels can answer. # # bash tests/screenshot.sh res://tests/_skill_menu_preview.tscn /tmp/m.png 20 720x1280 # ... and again at 720x960 to check a shorter screen format. @@ -17,15 +17,13 @@ func _ready() -> void: GameState.unlocked = ["shipyards", "logistics", "freight"] var main: Node = load("res://Main.tscn").instantiate() add_child(main) - main.get_node("BuildingField").place(GameState.BUILDING_TRANSPORT_HUB, Vector2(400.0, 0.0)) + main.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0)) + var mothership_id: int = int(GameState.buildings[-1].id) var bc: Node = main.get_node("BuildController") - bc.craft_building_id = 1 + for bar in bc.resource_bars: + bar.building_id = mothership_id main.get_node("UI/SkillMenu").visible = true - if "Skill" == "Craft": - bc._refresh_craft_status() - else: - bc._refresh_load_ore() - # Select a node so the description, price and status all have text - - # an empty description cannot collide with anything. - bc.skill_tree.selected_id = "freight" - bc._refresh_skill_menu("freight") + # Select a node so the description, price and status all have text - an + # empty description cannot collide with anything. + bc.skill_tree.selected_id = "freight" + bc._refresh_skill_menu("freight") diff --git a/tests/_transfer_menu_preview.gd b/tests/_transfer_menu_preview.gd new file mode 100644 index 0000000..3589a88 --- /dev/null +++ b/tests/_transfer_menu_preview.gd @@ -0,0 +1,27 @@ +extends Node2D + +# Preview, not a test. Opens the TransferMenu (#24) so its layout can be +# checked on a real viewport - four buttons, two captions and the shared +# header have to coexist without collisions, which no headless test can see. +# +# bash tests/screenshot.sh res://tests/_transfer_menu_preview.tscn /tmp/m.png 20 720x1280 +# ... and again at 720x960 to check a shorter screen format. + + +func _ready() -> void: + GameState.buildings.clear() + GameState.extractors.clear() + GameState.transporters.clear() + GameState.rocks = 180 + GameState.fleet_cargo = 24 + 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) + var bc: Node = main.get_node("BuildController") + bc.transfer_building_id = crafter_id + main.get_node("UI/TransferMenu").visible = true + bc._refresh_transfer() + bc.transfer_status.text = "Moved 25 ore into this building" diff --git a/tests/_transfer_menu_preview.gd.uid b/tests/_transfer_menu_preview.gd.uid new file mode 100644 index 0000000..b3af1e8 --- /dev/null +++ b/tests/_transfer_menu_preview.gd.uid @@ -0,0 +1 @@ +uid://b8omh1o4ysy6p diff --git a/tests/_transfer_menu_preview.tscn b/tests/_transfer_menu_preview.tscn new file mode 100644 index 0000000..a2c4c8d --- /dev/null +++ b/tests/_transfer_menu_preview.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/_transfer_menu_preview.gd" id="1"] + +[node name="TransferMenuPreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/test_cargo_readout.gd b/tests/test_cargo_readout.gd index 03b2500..02bae96 100644 --- a/tests/test_cargo_readout.gd +++ b/tests/test_cargo_readout.gd @@ -27,6 +27,7 @@ var scene: Node var banked: Control var cargo: Control var stage: int = 0 +var mothership_id: int = 0 var elapsed: float = 0.0 @@ -36,6 +37,11 @@ func _initialize() -> void: game_state.extractors.clear() game_state.rocks = 42 game_state.fleet_cargo = 0 + # Banking is a transfer TO the mothership now (#24), and a transfer needs + # a building to aim at. Only the record is required - the transfer + # primitives work off GameState, not off the spawned node - so this stays + # a display test rather than becoming one about the building system. + mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) scene = load("res://Main.tscn").instantiate() root.add_child(scene) banked = scene.get_node("UI/ResourceCounter") @@ -95,7 +101,7 @@ func _process(delta: float) -> bool: ]) # --- 4. delivery moves it across ---------------------------- - game_state.deliver_cargo() + game_state.transfer_to_building(mothership_id, game_state.fleet_cargo) elapsed = 0.0 stage = 2 return false diff --git a/tests/test_crafter_feedback.gd b/tests/test_crafter_feedback.gd index 87f7d0b..3e779fc 100644 --- a/tests/test_crafter_feedback.gd +++ b/tests/test_crafter_feedback.gd @@ -5,29 +5,28 @@ extends SceneTree # Every press in the ship crafter must say what happened. Reported twice from # play-testing as "the buttons do nothing". # -# There is no silent-failure path left to press: storing with an empty hold, -# ordering something the crafter cannot pay for yet, and storing successfully -# all report. The queue in particular is invisible by nature - an order that -# cannot be afforded is accepted and waits (#16), which looks exactly like a -# dead button unless it says so. +# The queue is invisible by nature - an order that cannot be afforded is +# accepted and waits (#16), which looks exactly like a dead button unless it +# says so. # # Verifies: -# 1. storing with an empty hold explains where ore comes from -# 2. ordering against an empty stock says it is queued AND how much more +# 1. ordering against an empty stock says it is queued AND how much more # ore the building needs -# 3. a successful store reports the amount -# 4. the message survives the per-frame status refresh - it used to be +# 2. the message survives the per-frame status refresh - it used to be # overwritten on the very next frame, which is what made a working # button look dead -# 5. the message is the FIRST line, not buried under the standing state -# 6. the panel is opened the way the GAME opens it - park the fleet on the -# crafter and press the action button - and every button then works +# 3. the message is the FIRST line, not buried under the standing state +# 4. the panel is opened the way the GAME opens it - park the fleet on the +# crafter and press the action button - and its buttons then work # -# Point 6 exists because this test used to set craft_building_id by hand. +# Point 4 exists because this test used to set craft_building_id by hand. # That skipped the one step that was actually broken: opening the panel # stopped recording WHICH crafter, so every handler bailed out at its `== 0` # guard while the panel still opened. Reported four times as "the buttons do # nothing", and invisible to every test that set the id itself. +# +# Storing ore used to be tested here too. It moved out of this panel entirely +# in #24 - see test_resource_transfer.gd. var game_state: Node var scene: Node @@ -44,7 +43,11 @@ func _initialize() -> void: game_state.extractors.clear() game_state.transporters.clear() game_state.reset_progression() - game_state.unlocked = ["shipyards", "coordination", "wing"] + # Every capacity node, so the interceptor button stays available for the + # real-tap checks below. A full fleet correctly DISABLES it, and a + # disabled button not reacting to a tap is not the bug those checks are + # looking for. + game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"] game_state.rocks = 100 game_state.fleet_cargo = 0 game_state.vessels_owed.clear() @@ -104,17 +107,7 @@ func _process(delta: float) -> bool: return true print("panel opened from the building, bound to crafter %d" % controller.craft_building_id) - # --- 1. store with an empty hold ---------------------------- - controller._on_store_ore() - var empty_msg: String = _first_line() - if "carrying nothing" not in empty_msg: - print("got: %s" % empty_msg) - print("TEST_CRAFTER_FEEDBACK: FAIL (storing with an empty hold said nothing useful)") - quit(1) - return true - print("empty hold -> %s" % empty_msg) - - # --- 2. order against an empty stock ------------------------ + # --- 1. order against an empty stock ------------------------ controller._on_craft_interceptor() var queued_msg: String = _first_line() if "queued" not in queued_msg.to_lower() or "more ore" not in queued_msg: @@ -127,71 +120,47 @@ func _process(delta: float) -> bool: quit(1) return true print("unaffordable order -> %s" % queued_msg) - - # --- 3. a successful store ---------------------------------- - game_state.withdraw_to_fleet(30) - controller._on_store_ore() - var stored_msg: String = _first_line() - if "Stored" not in stored_msg: - print("got: %s" % stored_msg) - print("TEST_CRAFTER_FEEDBACK: FAIL (a successful store did not report)") - quit(1) - return true - print("successful store -> %s" % stored_msg) elapsed = 0.0 stage = 2 return false 2: - # --- 4 + 5. it survives the refresh, and stays first -------- + # --- 2 + 3. it survives the refresh, and stays first -------- if elapsed < 0.5: return false controller._refresh_craft_status() var still: String = _first_line() - if "Stored" not in still: + if "queued" not in still.to_lower(): print("first line half a second later: %s" % still) print("TEST_CRAFTER_FEEDBACK: FAIL (the message was overwritten by the per-frame refresh)") quit(1) return true print("half a second later, still first: %s" % still) - # --- 6. a REAL tap, through the input path ------------------ + # --- 4. a REAL tap, through the input path ------------------ # Everything above calls the handlers directly, which is how a # whole class of bug kept getting missed: the crafter logic was # fine and the BUTTON was inert, blocked by a modal guard. This # taps the control the way a touch does. - game_state.withdraw_to_fleet(20) - var stock_before: int = game_state.building_stock(crafter_id) - var store_button: Control = scene.get_node("UI/CraftMenu/StoreOreButton") - for pressed in [true, false]: - var ev: InputEventScreenTouch = InputEventScreenTouch.new() - ev.pressed = pressed - ev.position = store_button.size / 2.0 - store_button._gui_input(ev) - if game_state.building_stock(crafter_id) <= stock_before: - print("stock %d -> %d after tapping Store ore" % [ - stock_before, game_state.building_stock(crafter_id) + var queued_before: int = game_state.pending_orders(crafter_id) + _tap(scene.get_node("UI/CraftMenu/InterceptorButton")) + if game_state.pending_orders(crafter_id) <= queued_before: + print("orders %d -> %d after tapping Interceptor" % [ + queued_before, game_state.pending_orders(crafter_id) ]) - print("TEST_CRAFTER_FEEDBACK: FAIL (tapping the store button did nothing)") + print("TEST_CRAFTER_FEEDBACK: FAIL (tapping the interceptor button did nothing)") quit(1) return true - print("real tap on Store ore moved %d ore into the crafter" % [ - game_state.building_stock(crafter_id) - stock_before - ]) + print("real tap on Interceptor queued another order") # And the same tap must still work after the build menu has been # opened and closed, which used to block every button forever. controller._on_build_pressed() controller._show_building_info(GameState.BUILDING_SHIP_CRAFTER) scene.get_node("UI/BuildMenu").visible = false - game_state.withdraw_to_fleet(10) - var stock_mid: int = game_state.building_stock(crafter_id) - for pressed2 in [true, false]: - var ev2: InputEventScreenTouch = InputEventScreenTouch.new() - ev2.pressed = pressed2 - ev2.position = store_button.size / 2.0 - store_button._gui_input(ev2) - if game_state.building_stock(crafter_id) <= stock_mid: - print("TEST_CRAFTER_FEEDBACK: FAIL (store button dead after the build menu was opened and closed)") + var queued_mid: int = game_state.pending_orders(crafter_id) + _tap(scene.get_node("UI/CraftMenu/InterceptorButton")) + if game_state.pending_orders(crafter_id) <= queued_mid: + print("TEST_CRAFTER_FEEDBACK: FAIL (craft button dead after the build menu was opened and closed)") quit(1) return true print("still works after opening and closing the build menu") diff --git a/tests/test_fleet_cargo.gd b/tests/test_fleet_cargo.gd index 3b395ec..0607cc7 100644 --- a/tests/test_fleet_cargo.gd +++ b/tests/test_fleet_cargo.gd @@ -12,7 +12,10 @@ extends SceneTree # # Verifies: # 1. mining fills the hold and leaves the bank alone -# 2. reaching the mothership banks the lot, and empties the hold +# 2. unloading at the mothership banks the lot and empties the hold - but +# only when the player asks. Arriving used to do it by itself; #24 +# removed that, so this now checks BOTH halves: parking changes +# nothing, and the explicit transfer moves everything # 3. with no mothership there is nowhere to deliver, so cargo just # accumulates - it is never quietly banked anyway # 4. sitting on a DIFFERENT building is not a delivery @@ -27,6 +30,7 @@ var field: Node var stage: int = 0 var elapsed: float = 0.0 var banked_before: int = 0 +var mothership_id: int = 0 const MOTHERSHIP_AT: Vector2 = Vector2(1200.0, 0.0) const CRAFTER_AT: Vector2 = Vector2(-4000.0, 0.0) @@ -117,6 +121,7 @@ func _process(delta: float) -> bool: # --- 4. a different building is not a delivery point -------- field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT) field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT) + mothership_id = int(game_state.buildings[-1].id) _park_fleet_on(CRAFTER_AT) elapsed = 0.0 stage = 2 @@ -139,6 +144,14 @@ func _process(delta: float) -> bool: if elapsed < 0.4: return false var total_before: int = banked_before + 3 + # Parking alone must do nothing now (#24). + if game_state.fleet_cargo != 3 or game_state.rocks != banked_before: + print("TEST_FLEET_CARGO: FAIL (arriving at the mothership banked the hold by itself)") + quit(1) + return true + print("parked on the mothership -> hold still %d (no auto-banking)" % game_state.fleet_cargo) + + game_state.transfer_to_building(mothership_id, 999) print("delivered: hold=%d bank=%d (expected hold=0 bank=%d)" % [ game_state.fleet_cargo, game_state.rocks, total_before ]) @@ -156,10 +169,6 @@ func _process(delta: float) -> bool: # Not simulated by loading PlanetSurface.tscn here: the point is # that cargo lives in the autoload rather than in Main.tscn, so # tearing Main.tscn down must not touch it. - # Fly off the mothership first, or this new cargo is (correctly) - # banked the moment BuildingField next runs, and the teardown - # check below would be measuring a delivery instead. - _park_fleet_on(CRAFTER_AT) game_state.collect_cargo(5) scene.queue_free() elapsed = 0.0 diff --git a/tests/test_manual_supply_route.gd b/tests/test_manual_supply_route.gd index c2f9c2a..c9181d9 100644 --- a/tests/test_manual_supply_route.gd +++ b/tests/test_manual_supply_route.gd @@ -14,9 +14,10 @@ extends SceneTree # Verifies: # 1. withdrawing moves ore bank -> hold, and conserves the total # 2. withdrawing is capped by what is actually banked (no overdraft) -# 3. arriving at the mothership banks the hold, but doing so is EDGE -# triggered - ore withdrawn while parked there is not instantly taken -# back, which would make the button look broken +# 3. arriving at the mothership does NOT bank the hold. It used to, edge +# triggered on arrival (#14); #24 removed that entirely, because an +# automatic one-way move overrides a player who has just loaded the +# hold in order to carry ore somewhere else # 4. a building takes the fleet's whole hold when opened # 5. end to end: a queued order the bank cannot reach is fulfilled using # only the fleet, with no Transport Hub in existence @@ -28,6 +29,7 @@ var fleet: Node var field: Node var controller: Node var crafter_id: int = 0 +var mothership_id: int = 0 var stage: int = 0 var elapsed: float = 0.0 var ships_before: int = 0 @@ -74,6 +76,7 @@ func _process(delta: float) -> bool: _clear_hazards() fleet.spawn_ships(2, MOTHERSHIP_AT) field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT) + mothership_id = int(game_state.buildings[-1].id) field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT) crafter_id = int(game_state.buildings[-1].id) total_before = _total() @@ -107,7 +110,7 @@ func _process(delta: float) -> bool: if elapsed < 0.3: return false var bank_before: int = game_state.rocks - var took: int = game_state.withdraw_to_fleet(GameState.SHIP_COST) + var took: int = game_state.transfer_to_fleet(mothership_id, GameState.SHIP_COST) if took != GameState.SHIP_COST or game_state.fleet_cargo != GameState.SHIP_COST: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (withdrew %d into a hold of %d)" % [took, game_state.fleet_cargo]) quit(1) @@ -123,14 +126,22 @@ func _process(delta: float) -> bool: stage = 4 return false 4: - # --- 3. still parked on the mothership ---------------------- + # --- 3. parked on the mothership, and arriving again -------- if elapsed < 0.5: return false if game_state.fleet_cargo != GameState.SHIP_COST: - print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore withdrawn while parked on the mothership was instantly banked again - delivery is not edge triggered)") + print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore withdrawn while parked on the mothership was banked again by itself)") quit(1) return true - print("still parked -> hold kept its %d (delivery is edge triggered)" % game_state.fleet_cargo) + # Leaving and coming back is the case the old edge-triggered + # delivery fired on, so it is the one worth re-entering. + _park_fleet_on(CRAFTER_AT) + _park_fleet_on(MOTHERSHIP_AT) + if game_state.fleet_cargo != GameState.SHIP_COST: + print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (arriving at the mothership auto-banked the hold - #24 removed that)") + quit(1) + return true + print("hold kept its %d across an arrival (no auto-banking)" % game_state.fleet_cargo) # --- 4. carry it to the crafter and open it ----------------- _park_fleet_on(CRAFTER_AT) @@ -140,7 +151,7 @@ func _process(delta: float) -> bool: 5: if elapsed < 0.6: return false - var moved: int = game_state.unload_into_building(crafter_id) + var moved: int = game_state.transfer_to_building(crafter_id, game_state.fleet_cargo) if moved != GameState.SHIP_COST or game_state.fleet_cargo != 0: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (unloaded %d, hold left at %d)" % [moved, game_state.fleet_cargo]) quit(1) @@ -180,7 +191,7 @@ func _process(delta: float) -> bool: # --- 2. no overdraft ---------------------------------------- game_state.rocks = 3 - var over: int = game_state.withdraw_to_fleet(999) + var over: int = game_state.transfer_to_fleet(mothership_id, 999) if over != 3 or game_state.rocks != 0: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (overdrew: took %d from a bank of 3, leaving %d)" % [over, game_state.rocks]) quit(1) diff --git a/tests/test_resource_transfer.gd b/tests/test_resource_transfer.gd new file mode 100644 index 0000000..4f0d2bd --- /dev/null +++ b/tests/test_resource_transfer.gd @@ -0,0 +1,219 @@ +extends SceneTree + +# Headless gameplay test: run with +# godot --headless -s res://tests/test_resource_transfer.gd +# Forgejo #24: moving ore between the fleet and a building is its own +# top-level action, not something buried inside the ship crafter or the +# research screen. +# +# Verifies: +# 1. the transfer button appears alongside the action button whenever the +# fleet is standing on a building, and opens the transfer panel bound to +# THAT building - the `== 0` guard failure that made every crafter +# button look dead is the same trap here +# 2. ore moves both ways at an ordinary building, capped by what is +# actually at each end (no overdraft in either direction) +# 3. at a mothership the building's holdings ARE the bank, so unloading +# there banks the ore and loading there draws on it +# 4. a REAL tap through the input path moves ore, not just the handler +# 5. pressing against an empty end reports why instead of doing nothing +# 6. the shared resource bar reports the building the panel was opened from +# 7. ore is conserved across the whole exercise + +var game_state: Node +var scene: Node +var fleet: Node +var controller: Node +var crafter_id: int = 0 +var mothership_id: int = 0 +var stage: int = 0 +var elapsed: float = 0.0 +var total_before: int = 0 +var failures: PackedStringArray = PackedStringArray() + +const MOTHERSHIP_AT: Vector2 = Vector2(0.0, 0.0) +const CRAFTER_AT: Vector2 = Vector2(3000.0, 0.0) + + +func _total() -> int: + var sum: int = game_state.rocks + game_state.fleet_cargo + for b in game_state.buildings: + if b.type != GameState.BUILDING_MOTHERSHIP: + sum += int(b.get("stock", 0)) + return sum + + +func _initialize() -> void: + game_state = root.get_node("GameState") + game_state.buildings.clear() + game_state.extractors.clear() + game_state.transporters.clear() + game_state.rocks = 100 + game_state.fleet_cargo = 0 + game_state.vessels_owed.clear() + game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"] + scene = load("res://Main.tscn").instantiate() + root.add_child(scene) + fleet = scene.get_node("Fleet") + controller = scene.get_node("BuildController") + + +func _park_fleet_on(at: Vector2) -> void: + for ship in fleet.ships: + ship.position = at + fleet.move_to(at) + + +func _tap(button: Control) -> void: + for pressed in [true, false]: + var ev: InputEventScreenTouch = InputEventScreenTouch.new() + ev.pressed = pressed + ev.position = button.size / 2.0 + button._gui_input(ev) + + +func _expect(condition: bool, message: String) -> void: + if not condition: + failures.append("stage %d: %s" % [stage, message]) + + +func _process(delta: float) -> bool: + elapsed += delta + match stage: + 0: + scene.get_node("AsteroidField").set_process(false) + scene.get_node("EnemySpawner").set_process(false) + _clear_hazards() + fleet.spawn_ships(2, MOTHERSHIP_AT) + var field: Node = scene.get_node("BuildingField") + field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT) + mothership_id = int(game_state.buildings[-1].id) + field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT) + crafter_id = int(game_state.buildings[-1].id) + total_before = _total() + _park_fleet_on(CRAFTER_AT) + elapsed = 0.0 + stage = 1 + return false + + 1: + # --- 1. the button appears, and binds to this building ------ + if elapsed < 1.2: + return false + _expect(controller.current_building != null, + "parking on the crafter never registered") + _expect(controller.transfer_button.visible, + "the transfer button did not appear at a building") + _tap(controller.transfer_button) + _expect(controller.transfer_menu.visible, + "tapping the transfer button did not open the panel") + _expect(controller.transfer_building_id == crafter_id, + "panel bound to building %d, crafter is %d" + % [controller.transfer_building_id, crafter_id]) + + # --- 6. the shared bar follows the panel -------------------- + for bar in controller.resource_bars: + _expect(bar.building_id == crafter_id, + "a resource bar is reporting building %d, not the crafter %d" + % [bar.building_id, crafter_id]) + + # --- 5. an empty hold explains itself ----------------------- + game_state.fleet_cargo = 0 + controller._on_transfer(true, GameState.CARGO_LOAD_BATCH) + _expect("carrying nothing" in controller.transfer_status.text, + "unloading an empty hold said: %s" % controller.transfer_status.text) + + # --- 2. both directions at an ordinary building ------------- + # Baseline taken AFTER the last hand-set figure: the conservation + # check at the end is about the transfers, and ore conjured into + # the hold to set the scene would otherwise read as a leak. + game_state.fleet_cargo = 40 + total_before = _total() + controller._on_transfer(true, GameState.CARGO_LOAD_BATCH) + _expect(game_state.building_stock(crafter_id) == GameState.CARGO_LOAD_BATCH, + "a batch into the crafter left stock at %d" + % game_state.building_stock(crafter_id)) + _expect(game_state.fleet_cargo == 40 - GameState.CARGO_LOAD_BATCH, + "the hold paid %d, not the batch" % (40 - game_state.fleet_cargo)) + + controller._on_transfer(false, 999) + _expect(game_state.building_stock(crafter_id) == 0, + "loading ALL left %d in the crafter" % game_state.building_stock(crafter_id)) + _expect(game_state.fleet_cargo == 40, + "the hold came back to %d, not 40" % game_state.fleet_cargo) + + # Asking for more than exists moves only what is there. + var over: int = game_state.transfer_to_fleet(crafter_id, 999) + _expect(over == 0, "overdrew %d from an empty crafter" % over) + + _park_fleet_on(MOTHERSHIP_AT) + controller.transfer_menu.visible = false + elapsed = 0.0 + stage = 2 + return false + + 2: + # --- 3. the mothership's holdings are the bank -------------- + if elapsed < 1.2: + return false + _expect(controller.transfer_button.visible, + "the transfer button did not appear at the mothership") + _tap(controller.transfer_button) + _expect(controller.transfer_building_id == mothership_id, + "panel bound to %d, mothership is %d" + % [controller.transfer_building_id, mothership_id]) + _expect(game_state.building_holdings(mothership_id) == game_state.rocks, + "mothership holdings %d != bank %d" + % [game_state.building_holdings(mothership_id), game_state.rocks]) + + var bank_before: int = game_state.rocks + var hold_before: int = game_state.fleet_cargo + controller._on_transfer(true, 999) + _expect(game_state.fleet_cargo == 0, + "unloading ALL at the mothership left %d in the hold" % game_state.fleet_cargo) + _expect(game_state.rocks == bank_before + hold_before, + "the bank went to %d, expected %d" % [game_state.rocks, bank_before + hold_before]) + + # --- 4. a REAL tap, through the input path ------------------ + # 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/ToFleetBatchButton")) + _expect(game_state.fleet_cargo == GameState.CARGO_LOAD_BATCH, + "a real tap loaded %d, expected a batch of %d" + % [game_state.fleet_cargo, GameState.CARGO_LOAD_BATCH]) + _expect(game_state.rocks == bank_at_tap - GameState.CARGO_LOAD_BATCH, + "the bank did not pay for the tapped withdrawal") + + # --- 7. conservation ---------------------------------------- + _expect(_total() == total_before, + "ore created or destroyed: %d -> %d" % [total_before, _total()]) + + _finish() + return true + return false + + +func _finish() -> void: + if failures.is_empty(): + print("TEST_RESOURCE_TRANSFER: PASS") + quit(0) + else: + print("bank=%d hold=%d crafter=%d" % [ + game_state.rocks, game_state.fleet_cargo, game_state.building_stock(crafter_id) + ]) + for f in failures: + print("FAIL: %s" % f) + print("TEST_RESOURCE_TRANSFER: FAIL") + quit(1) + + +func _clear_hazards() -> void: + # Drifting rocks can destroy a parked ship and change the fleet size out + # from under a logistics test. Same reason as test_manual_supply_route. + for a in get_nodes_in_group("asteroids"): + if is_instance_valid(a): + a.queue_free() + for e in get_nodes_in_group("enemies"): + if is_instance_valid(e): + e.queue_free() diff --git a/tests/test_resource_transfer.gd.uid b/tests/test_resource_transfer.gd.uid new file mode 100644 index 0000000..e9b5788 --- /dev/null +++ b/tests/test_resource_transfer.gd.uid @@ -0,0 +1 @@ +uid://j27f3sjneewj