Remove the one-way ore API superseded by the transfer menu
The loose end from #24. withdraw_to_fleet, unload_into_building and deliver_cargo were the old one-way moves behind "Load ore" and "Store ore here"; every one of them is a special case of transfer_to_building / transfer_to_fleet, and none had a caller left outside tests. The cargo_delivered signal had no listeners at all. Not just tidying: unload_into_building routed through deposit_to_building, which does NOT special-case the mothership - it writes b.stock, while building_holdings() returns `rocks` for a mothership and ignores stock entirely. So unload_into_building( mothership_id) would have parked ore in a field nothing reads, with no error. Nothing called it that way, but it is a plausibly-named function sitting there to be reached for later. transfer_to_building handles it. CARGO_LOAD_BATCH stays - the transfer menu's batch buttons use it; only its role as withdraw_to_fleet's default argument is gone. The #24 note expected folding these in to be costly for test_cargo_readout, which places no buildings. It is not: the transfer primitives work off GameState records rather than spawned nodes, so a single add_building() in setup is enough and it stays a display test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
fe1c721d64
commit
b6427b080d
27 changed files with 1085 additions and 275 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
81
GameState.gd
81
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
328
Main.tscn
328
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
|
||||
|
|
|
|||
111
ResourceBar.gd
Normal file
111
ResourceBar.gd
Normal file
|
|
@ -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
|
||||
)
|
||||
1
ResourceBar.gd.uid
Normal file
1
ResourceBar.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c3x1o7bh4tl5
|
||||
BIN
shots/craft.png
Normal file
BIN
shots/craft.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
40
shots/craft.png.import
Normal file
40
shots/craft.png.import
Normal file
|
|
@ -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
|
||||
BIN
shots/lab.png
Normal file
BIN
shots/lab.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
40
shots/lab.png.import
Normal file
40
shots/lab.png.import
Normal file
|
|
@ -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
|
||||
BIN
shots/skill.png
Normal file
BIN
shots/skill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
40
shots/skill.png.import
Normal file
40
shots/skill.png.import
Normal file
|
|
@ -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
|
||||
BIN
shots/transfer.png
Normal file
BIN
shots/transfer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
40
shots/transfer.png.import
Normal file
40
shots/transfer.png.import
Normal file
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
# 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")
|
||||
|
|
|
|||
27
tests/_transfer_menu_preview.gd
Normal file
27
tests/_transfer_menu_preview.gd
Normal file
|
|
@ -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"
|
||||
1
tests/_transfer_menu_preview.gd.uid
Normal file
1
tests/_transfer_menu_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b8omh1o4ysy6p
|
||||
6
tests/_transfer_menu_preview.tscn
Normal file
6
tests/_transfer_menu_preview.tscn
Normal file
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
219
tests/test_resource_transfer.gd
Normal file
219
tests/test_resource_transfer.gd
Normal file
|
|
@ -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()
|
||||
1
tests/test_resource_transfer.gd.uid
Normal file
1
tests/test_resource_transfer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://j27f3sjneewj
|
||||
Loading…
Reference in a new issue